Coverage for src/invariant/cacheable.py: 96.97%

1"""Cacheable type boundary: the single source of truth for allowed types.

2

3 This module defines the Cacheable Type Universe and provides the core function

4 that validates values throughout the Invariant system.

5

6 The Allowed Types (recursive for containers):

7 - int, str, bool, None

8 - Decimal (safe numerics — no float!)

9 - dict[str, CacheableValue] (string keys only)

10 - list[CacheableValue], tuple[CacheableValue, ...]

11 - Any ICacheable implementor (domain types like Polynomial)

12

13 FORBIDDEN: float (IEEE 754 non-determinism), bytes, arbitrary objects

14

15 Native types are stored directly without wrapping. The store codec handles

16 serialization of all cacheable types uniformly.

17"""

18

19 from collections.abc import Mapping, Sequence

20 from decimal import Decimal

21 from typing import Any

22

23 from invariant.protocol import ICacheable

24

25

26 def is_cacheable(value: Any) -> bool:

27 """Check if a value belongs to the Cacheable Type Universe.

28

29 This is the authoritative predicate for determining whether a value

30 can be used in manifests, stored as artifacts, or passed between nodes.

31 It recursively validates containers to ensure all nested values are cacheable.

32

33 Args:

34 value: The value to check.

35

36 Returns:

37 True if the value is cacheable, False otherwise.

38

39 Examples:

40 >>> is_cacheable(42)

41 True

42 >>> is_cacheable("hello")

43 True

44 >>> is_cacheable(3.14) # float is forbidden

45 False

46 >>> is_cacheable({"a": 1, "b": 2})

47 True

48 >>> is_cacheable([1, 2, 3])

49 True

50 >>> is_cacheable({"a": 1.5}) # nested float is forbidden

51 False

52 """

53 # None is cacheable

54 if value is None:

55 return True

56

57 # ICacheable implementors are always cacheable

58 if isinstance(value, ICacheable):

59 return True

60

61 # Primitives: int, str, bool

62 if isinstance(value, (int, str, bool)):

63 return True

64

65 # Decimal is cacheable (safe numeric, unlike float)

66 if isinstance(value, Decimal):

67 return True

68

69 # FORBIDDEN: float (IEEE 754 non-determinism)

70 if isinstance(value, float):

71 return False

72

73 # FORBIDDEN: bytes

74 if isinstance(value, bytes):

75 return False

76

77 # Containers: dict with string keys

78 if isinstance(value, Mapping):

79 if not isinstance(value, dict):

80 # Only plain dict is allowed, not arbitrary Mapping

81 return False

82 # All keys must be strings

83 for key in value.keys():

84 if not isinstance(key, str):

85 return False

86 # All values must be cacheable (recursive)

87 for val in value.values():

88 if not is_cacheable(val):

89 return False

90 return True

91

92 # Containers: list, tuple

93 if isinstance(value, Sequence) and not isinstance(value, str):

94 # All elements must be cacheable (recursive)

95 for item in value:

96 if not is_cacheable(item):

97 return False

98 return True

99

100 # Everything else is forbidden

101 return False