diff --git a/decent_array/_constants.py b/decent_array/_constants.py index a05e32b..a53514d 100644 --- a/decent_array/_constants.py +++ b/decent_array/_constants.py @@ -2,9 +2,24 @@ import math -_CONSTANTS = ["e", "inf", "nan", "pi"] +_CONSTANTS = { + "e": math.e, + "inf": math.inf, + "nan": math.nan, + "pi": math.pi, +} -e = math.e -inf = math.inf -nan = math.nan -pi = math.pi +e = _CONSTANTS["e"] +inf = _CONSTANTS["inf"] +nan = _CONSTANTS["nan"] +pi = _CONSTANTS["pi"] + + +def _reset() -> None: + """Set/reset constants to the Python math defaults.""" + global e, inf, nan, pi # noqa: PLW0603 + + e = _CONSTANTS["e"] + inf = _CONSTANTS["inf"] + nan = _CONSTANTS["nan"] + pi = _CONSTANTS["pi"] diff --git a/decent_array/interoperability/_backend_manager.py b/decent_array/interoperability/_backend_manager.py index c897fea..658f5aa 100644 --- a/decent_array/interoperability/_backend_manager.py +++ b/decent_array/interoperability/_backend_manager.py @@ -131,6 +131,17 @@ def reset_backends() -> None: for listener in _BACKEND_LISTENERS: listener(None) + # clear dtypes bindings + for dt in dtypes._ALL_DTYPES: # noqa: SLF001 + dt._available = False # noqa: SLF001 + dt._backend_dtype = None # noqa: SLF001 + + dtypes._AVAILABLE_DTYPES.clear() # noqa: SLF001 + dtypes._BACKEND_DTYPE_TO_DTYPE.clear() # noqa: SLF001 + + # clear constants bindings, restoring to Python math defaults + constants._reset() # noqa: SLF001 + def default_device() -> Devices: """ @@ -219,6 +230,14 @@ def _bind_dtypes(backend: Backend | None) -> None: dt._available = backend_dt is not None # noqa: SLF001 dt._backend_dtype = backend_dt # noqa: SLF001 + # refresh caches of available dtypes and reverse mapping + dtypes._AVAILABLE_DTYPES.clear() # noqa: SLF001 + dtypes._AVAILABLE_DTYPES.update(dt for dt in dtypes._ALL_DTYPES if dt.available) # noqa: SLF001 + + dtypes._BACKEND_DTYPE_TO_DTYPE.clear() # noqa: SLF001 + for dt in dtypes._AVAILABLE_DTYPES: # noqa: SLF001 + dtypes._BACKEND_DTYPE_TO_DTYPE[dt.backend_dtype] = dt # noqa: SLF001 + def _bind_constants(backend: Backend | None) -> None: """Bind constants to the corresponding backend constants.""" @@ -226,6 +245,5 @@ def _bind_constants(backend: Backend | None) -> None: return for name in constants._CONSTANTS: # noqa: SLF001 backend_c = getattr(backend, name, None) - if backend_c is None: - return - setattr(constants, name, backend_c) + if backend_c is not None: + setattr(constants, name, backend_c) diff --git a/decent_array/types/_dtypes.py b/decent_array/types/_dtypes.py index f8dec4c..63334c8 100644 --- a/decent_array/types/_dtypes.py +++ b/decent_array/types/_dtypes.py @@ -74,7 +74,7 @@ def __eq__(self, other: object) -> bool: """Check equivalence by ``name`` attributes.""" if not isinstance(other, dtype): return NotImplemented - return self.name == other.name and self.available and other.available + return self.name == other.name def __hash__(self) -> int: """Hash of the dtype.""" @@ -131,12 +131,9 @@ def __hash__(self) -> int: _ALL_DTYPES = _BOOL_DTYPES | _NUMERIC_DTYPES | _MISCELLANEOUS_DTYPES -_AVAILABLE_DTYPES = {dt for dt in _ALL_DTYPES if dt.available} - - +# caches for available dtypes and reverse mapping; these are populated during set_backend +_AVAILABLE_DTYPES: set[dtype] = set() _BACKEND_DTYPE_TO_DTYPE: dict[Any, dtype] = {} -for dt in _AVAILABLE_DTYPES: - _BACKEND_DTYPE_TO_DTYPE[dt._backend_dtype] = dt # noqa: SLF001 _ALIASES = { diff --git a/pyproject.toml b/pyproject.toml index 937c05c..738be87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "decent-array" -version = "0.2.3" +version = "0.2.4" authors = [{name = "Simon Granström"}, {name = "Nicola Bastianello"}] maintainers = [{name = "Team Decent"}] description = "A library of array operations and linear algebra primitives for interoperability across ML frameworks." diff --git a/tests/test_backend_manager.py b/tests/test_backend_manager.py index 8e890c6..98e153c 100644 --- a/tests/test_backend_manager.py +++ b/tests/test_backend_manager.py @@ -98,8 +98,15 @@ def test_set_backend_instantiates_dtypes() -> None: dt2 = float32 # global dtype bound during set_dtype dt3 = dtype("float32") # backend is set, so this is bound to backend dtype - assert dt1 != dt2 # dtypes are not equal if not avaiable, and dt1 is not available - assert dt2 == dt3 # available because bound to backend, and equal + # check availability status + assert not dt1.available + assert dt2.available + assert dt3.available + + # equality check the name attribute, so all these should be true (irrespective of availability) + assert dt1 == dt2 + assert dt1 == dt3 + assert dt2 == dt3 dt4 = dtype("int16") assert dt3 != dt4 diff --git a/tests/test_bindings.py b/tests/test_bindings.py new file mode 100644 index 0000000..4c13e7c --- /dev/null +++ b/tests/test_bindings.py @@ -0,0 +1,92 @@ +"""Tests for dtype initialization and backend binding.""" + +from __future__ import annotations + +import math +import pytest + +from decent_array.interoperability import _backend_manager +from decent_array.interoperability._backend_manager import reset_backends +from decent_array.types import _dtypes +import decent_array._constants as constants + + +@pytest.fixture(autouse=True) +def reset_backend_state(): + """Reset backend state before and after each test.""" + reset_backends() + yield + reset_backends() + + +def test_all_dtypes_are_bound_to_backend(backend: tuple) -> None: + """Every dtype is bound to the corresponding backend attribute.""" + backend_instance = _backend_manager._BACKEND_INSTANCE + + assert backend_instance is not None + + for name in _dtypes._SUPPORTED: + dt = getattr(_dtypes, name) + expected_backend_dtype = getattr(backend_instance, name, None) + + assert dt.backend_dtype is expected_backend_dtype + assert dt.available is (expected_backend_dtype is not None) + + +def test_available_dtypes_cache_is_correct(backend: tuple) -> None: + expected = { + dt for dt in _dtypes._ALL_DTYPES if dt.available + } + + assert _dtypes._AVAILABLE_DTYPES == expected + + +def test_dtypes_returns_only_available_dtypes(backend: tuple) -> None: + available = _dtypes.dtypes() + + assert available + assert all(dt.available for dt in available.values()) + assert set(available.values()) == _dtypes._AVAILABLE_DTYPES + + +def test_backend_dtype_mapping_is_correct(backend: tuple) -> None: + expected = { + dt.backend_dtype: dt + for dt in _dtypes._AVAILABLE_DTYPES + } + + assert _dtypes._BACKEND_DTYPE_TO_DTYPE == expected + + for dt in _dtypes._AVAILABLE_DTYPES: + assert ( + _dtypes._BACKEND_DTYPE_TO_DTYPE[dt.backend_dtype] + is dt + ) + + +def test_reset_clears_dtype_bindings(backend: tuple) -> None: + """Resetting backends clears dtype bindings and caches.""" + assert _backend_manager._BACKEND_INSTANCE is not None + assert _dtypes._AVAILABLE_DTYPES + + reset_backends() + + assert _backend_manager._BACKEND_INSTANCE is None + assert not _dtypes._AVAILABLE_DTYPES + assert not _dtypes._BACKEND_DTYPE_TO_DTYPE + + for dt in _dtypes._ALL_DTYPES: + assert dt.available is False + assert dt.backend_dtype is None + + +def test_reset_restores_default_constants(backend: tuple) -> None: + """Resetting the backend restores Python math defaults.""" + assert _backend_manager._BACKEND_INSTANCE is not None + + reset_backends() + + assert constants.e == math.e + assert constants.inf == math.inf + assert math.isnan(constants.nan) + assert constants.pi == math.pi