Coverage for src/arraybridge/array_geometry.py: 87%
31 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 11:15 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 11:15 +0000
1"""Framework-neutral array geometry inspection."""
3from __future__ import annotations
5from dataclasses import dataclass
6from typing import Any
8import numpy as np
10from arraybridge.array_payload import ArrayPayload
13@dataclass(frozen=True, slots=True)
14class ArrayGeometry:
15 """Concrete shape metadata without moving array data between frameworks."""
17 shape: tuple[int, ...]
19 @property
20 def ndim(self) -> int:
21 """Return the rank derived from the canonical shape."""
23 return len(self.shape)
25 @classmethod
26 def from_value(cls, value: Any) -> ArrayGeometry | None:
27 """Inspect an array or nominal payload without forcing host conversion."""
29 data = value.array_payload_data() if isinstance(value, ArrayPayload) else value
30 declared_shape = getattr(data, "shape", None)
31 if declared_shape is not None:
32 try:
33 return cls(tuple(int(axis_size) for axis_size in declared_shape))
34 except (TypeError, ValueError):
35 return None
37 try:
38 array = np.asarray(data)
39 except (TypeError, ValueError):
40 return None
41 return cls(tuple(int(axis_size) for axis_size in array.shape))
43 @classmethod
44 def require_from_value(
45 cls,
46 value: Any,
47 *,
48 value_name: str = "Value",
49 ) -> ArrayGeometry:
50 """Return concrete geometry or reject a value without an array shape."""
52 geometry = cls.from_value(value)
53 if geometry is None:
54 raise TypeError(
55 f"{value_name} requires concrete array geometry, got " f"{type(value).__name__}."
56 )
57 return geometry