Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions decent_array/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
24 changes: 21 additions & 3 deletions decent_array/interoperability/_backend_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -219,13 +230,20 @@ 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."""
if backend is 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)
9 changes: 3 additions & 6 deletions decent_array/types/_dtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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."
Expand Down
11 changes: 9 additions & 2 deletions tests/test_backend_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions tests/test_bindings.py
Original file line number Diff line number Diff line change
@@ -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
Loading