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

33 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-25 10:21 +0100

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

2 

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

4that validates values throughout the Invariant system. 

5 

6The 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 

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

14 

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

16serialization of all cacheable types uniformly. 

17""" 

18 

19from collections.abc import Mapping, Sequence 

20from decimal import Decimal 

21from typing import Any 

22 

23from invariant.protocol import ICacheable 

24 

25 

26def 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