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

1"""Framework-neutral array geometry inspection.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass 

6from typing import Any 

7 

8import numpy as np 

9 

10from arraybridge.array_payload import ArrayPayload 

11 

12 

13@dataclass(frozen=True, slots=True) 

14class ArrayGeometry: 

15 """Concrete shape metadata without moving array data between frameworks.""" 

16 

17 shape: tuple[int, ...] 

18 

19 @property 

20 def ndim(self) -> int: 

21 """Return the rank derived from the canonical shape.""" 

22 

23 return len(self.shape) 

24 

25 @classmethod 

26 def from_value(cls, value: Any) -> ArrayGeometry | None: 

27 """Inspect an array or nominal payload without forcing host conversion.""" 

28 

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 

36 

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

42 

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.""" 

51 

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