Coverage for src/seqrule/dsl.py: 58%
40 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-27 10:56 -0600
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-27 10:56 -0600
1"""
2DSL layer for building sequence rules.
4This module provides a high-level DSL for constructing sequence rules,
5including combinators and common rule patterns.
6"""
8from typing import TypeVar, Union
10from .core import FormalRule, FormalRuleProtocol, Sequence
11from .types import PredicateFunction
13T = TypeVar("T")
16class DSLRule:
17 """
18 DSLRule wraps a formal rule with a human-readable description.
20 This DSL layer allows domain experts to build high-level rules that are automatically
21 translated into the underlying formal model.
23 Attributes:
24 func: The underlying formal rule function
25 description: Human-readable description of the rule
26 _original_func: The original unwrapped function for inspection
28 Examples:
29 >>> def ascending_values(seq):
30 ... return all(seq[i]["value"] < seq[i+1]["value"] for i in range(len(seq)-1))
31 ...
32 >>> rule = DSLRule(ascending_values, "values must be ascending")
33 >>> rule(sequence) # Returns True if values ascend, False otherwise
34 """
36 def __init__(
37 self, func: Union[FormalRule, FormalRuleProtocol], description: str = ""
38 ):
39 """
40 Initialize a DSL rule with a function and description.
42 Args:
43 func: The rule function that takes a sequence and returns a boolean
44 description: Human-readable description of the rule's purpose
45 """
46 self.func = func
47 self.description = description
48 # Store the original function for inspection
49 if hasattr(func, "__wrapped__"):
50 self._original_func = func.__wrapped__
51 else:
52 self._original_func = func
54 def __call__(self, seq: Sequence) -> bool:
55 """
56 Apply the rule to a sequence.
58 Args:
59 seq: The sequence to evaluate
61 Returns:
62 bool: True if the sequence satisfies the rule, False otherwise
63 """
64 return self.func(seq)
66 def __and__(self, other: "DSLRule") -> "DSLRule":
67 """
68 Combine two rules with a logical AND operation.
70 Args:
71 other: Another DSL rule to combine with this one
73 Returns:
74 DSLRule: A new rule that requires both rules to be satisfied
75 """
76 return DSLRule(
77 lambda seq: self(seq) and other(seq),
78 f"({self.description} AND {other.description})",
79 )
81 def __or__(self, other: "DSLRule") -> "DSLRule":
82 """
83 Combine two rules with a logical OR operation.
85 Args:
86 other: Another DSL rule to combine with this one
88 Returns:
89 DSLRule: A new rule that requires either rule to be satisfied
90 """
91 return DSLRule(
92 lambda seq: self(seq) or other(seq),
93 f"({self.description} OR {other.description})",
94 )
96 def __invert__(self) -> "DSLRule":
97 """
98 Negate a rule with a logical NOT operation.
100 Returns:
101 DSLRule: A new rule that is satisfied when this rule is not
102 """
103 return DSLRule(lambda seq: not self(seq), f"(NOT {self.description})")
105 def __repr__(self) -> str:
106 """
107 String representation of the rule.
109 Returns:
110 str: A string showing the rule description
111 """
112 return f"DSLRule({self.description})"
114 def __get_original_func__(self):
115 """
116 Return the original function for inspection.
118 Returns:
119 Callable: The original unwrapped function
120 """
121 return self._original_func
124def if_then_rule(
125 condition: PredicateFunction, consequence: PredicateFunction
126) -> DSLRule:
127 """
128 Constructs a DSLRule that applies to every adjacent pair in a sequence.
130 If an object satisfies 'condition', then its immediate successor must satisfy 'consequence'.
132 Args:
133 condition: Predicate that must be satisfied by the first object
134 consequence: Predicate that must be satisfied by the second object
136 Returns:
137 DSLRule: A rule enforcing the if-then relationship
139 Examples:
140 >>> # If an object is red, then the next object must have value > 5
141 >>> rule = if_then_rule(
142 ... lambda obj: obj["color"] == "red",
143 ... lambda obj: obj["value"] > 5
144 ... )
145 """
146 desc = "if [condition] then [consequence]"
148 def rule(seq: Sequence) -> bool:
149 for i in range(len(seq) - 1):
150 if condition(seq[i]) and not consequence(seq[i + 1]):
151 return False
152 return True
154 return DSLRule(rule, desc)
157def check_range(
158 seq: Sequence, start: int, length: int, condition: PredicateFunction
159) -> bool:
160 """
161 Checks if a slice of the sequence satisfies a condition.
163 Args:
164 seq: The sequence to check
165 start: Starting index
166 length: Length of the slice
167 condition: Predicate that must be satisfied by each object
169 Returns:
170 bool: True if the slice satisfies the condition
171 """
172 if len(seq) < start + length:
173 return False
174 return all(condition(seq[i]) for i in range(start, start + length))
177def range_rule(start: int, length: int, condition: PredicateFunction) -> DSLRule:
178 """
179 Constructs a DSLRule requiring elements in an index range satisfy a condition.
181 Args:
182 start: Starting index
183 length: Length of the range
184 condition: Predicate that must be satisfied by each object
186 Returns:
187 DSLRule: A rule enforcing the condition over the range
189 Examples:
190 >>> # Require the first three elements to have even values
191 >>> rule = range_rule(0, 3, lambda obj: obj["value"] % 2 == 0)
192 """
193 desc = f"elements[{start}:{start+length}] satisfy condition"
194 return DSLRule(lambda seq: check_range(seq, start, length, condition), desc)
197def and_atomic(*conditions: PredicateFunction) -> PredicateFunction:
198 """
199 Combines multiple atomic predicates into one using logical AND.
201 Args:
202 *conditions: Variable number of predicates to combine
204 Returns:
205 Callable: A new predicate that is the conjunction of all inputs
207 Examples:
208 >>> # Object must be red AND have value > 5
209 >>> predicate = and_atomic(
210 ... lambda obj: obj["color"] == "red",
211 ... lambda obj: obj["value"] > 5
212 ... )
213 """
214 return lambda obj: all(cond(obj) for cond in conditions)