Coverage for src / invariant / types.py: 97.30%
37 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"""Domain types implementing ICacheable protocol.
3Native types (int, str, Decimal, dict, list) are supported directly without wrappers.
4Only domain types that require custom serialization implement ICacheable.
5"""
7import hashlib
8from typing import BinaryIO
10from invariant.protocol import ICacheable
13class Polynomial(ICacheable):
14 """A cacheable polynomial type.
16 Represents a polynomial as a tuple of integer coefficients, where the index
17 represents the degree (coefficient at index i is the coefficient of x^i).
19 Canonical form: Trailing zeros are stripped to ensure a unique representation.
20 For example, [1, 2, 0, 0] is canonicalized to [1, 2].
21 """
23 def __init__(self, coefficients: tuple[int, ...] | list[int]) -> None:
24 """Initialize with coefficient list.
26 Args:
27 coefficients: List or tuple of integer coefficients. Index i represents
28 the coefficient of x^i. Trailing zeros are automatically
29 stripped for canonical form.
30 """
31 # Convert to tuple and strip trailing zeros for canonical form
32 coeffs = tuple(coefficients)
33 # Strip trailing zeros
34 while len(coeffs) > 0 and coeffs[-1] == 0:
35 coeffs = coeffs[:-1]
36 # If all zeros, keep at least one zero
37 if len(coeffs) == 0:
38 coeffs = (0,)
39 self.coefficients = coeffs
41 def get_stable_hash(self) -> str:
42 """Return SHA-256 hash of the canonical coefficient tuple."""
43 # Hash the canonical coefficient tuple
44 coeff_bytes = b",".join(str(c).encode("utf-8") for c in self.coefficients)
45 return hashlib.sha256(coeff_bytes).hexdigest()
47 def to_stream(self, stream: BinaryIO) -> None:
48 """Serialize polynomial to stream.
50 Format: length-prefixed sequence of 8-byte signed integers (big-endian).
51 """
52 # Write length (number of coefficients)
53 stream.write(len(self.coefficients).to_bytes(8, byteorder="big", signed=False))
54 # Write each coefficient as 8-byte signed big-endian integer
55 for coeff in self.coefficients:
56 stream.write(coeff.to_bytes(8, byteorder="big", signed=True))
58 @classmethod
59 def from_stream(cls, stream: BinaryIO) -> "Polynomial":
60 """Deserialize polynomial from stream.
62 Reads length-prefixed sequence of 8-byte signed integers and strips
63 trailing zeros for canonical form.
64 """
65 # Read length
66 length = int.from_bytes(stream.read(8), byteorder="big", signed=False)
67 # Read coefficients
68 coefficients = []
69 for _ in range(length):
70 coeff = int.from_bytes(stream.read(8), byteorder="big", signed=True)
71 coefficients.append(coeff)
72 # Create polynomial (constructor will strip trailing zeros)
73 return cls(tuple(coefficients))
75 def __eq__(self, other: object) -> bool:
76 """Equality comparison."""
77 if not isinstance(other, Polynomial):
78 return False
79 return self.coefficients == other.coefficients
81 def __repr__(self) -> str:
82 """String representation."""
83 return f"Polynomial({list(self.coefficients)})"
85 def to_json_value(self) -> dict:
86 """Return JSON-serializable dict for graph serialization (IJsonRepresentable)."""
87 return {"coefficients": list(self.coefficients)}
89 @classmethod
90 def from_json_value(cls, obj: dict) -> "Polynomial":
91 """Reconstruct from JSON dict (IJsonRepresentable)."""
92 if "coefficients" not in obj:
93 raise ValueError("Polynomial from_json_value requires 'coefficients' key")
94 return cls(obj["coefficients"])