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
« 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.
3This module defines the Cacheable Type Universe and provides the core function
4that validates values throughout the Invariant system.
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)
13FORBIDDEN: float (IEEE 754 non-determinism), bytes, arbitrary objects
15Native types are stored directly without wrapping. The store codec handles
16serialization of all cacheable types uniformly.
17"""
19from collections.abc import Mapping, Sequence
20from decimal import Decimal
21from typing import Any
23from invariant.protocol import ICacheable
26def is_cacheable(value: Any) -> bool:
27 """Check if a value belongs to the Cacheable Type Universe.
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.
33 Args:
34 value: The value to check.
36 Returns:
37 True if the value is cacheable, False otherwise.
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
57 # ICacheable implementors are always cacheable
58 if isinstance(value, ICacheable):
59 return True
61 # Primitives: int, str, bool
62 if isinstance(value, (int, str, bool)):
63 return True
65 # Decimal is cacheable (safe numeric, unlike float)
66 if isinstance(value, Decimal):
67 return True
69 # FORBIDDEN: float (IEEE 754 non-determinism)
70 if isinstance(value, float):
71 return False
73 # FORBIDDEN: bytes
74 if isinstance(value, bytes):
75 return False
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
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
100 # Everything else is forbidden
101 return False