Coverage for src/seqrule/generators/patterns.py: 11%

54 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-27 10:39 -0600

1""" 

2Pattern-based sequence generation. 

3 

4This module provides functionality for pattern-based sequence generation 

5and pattern matching. 

6""" 

7 

8from typing import Any, Dict, List 

9 

10 

11class PropertyPattern: 

12 """ 

13 Represents a pattern of property values to match or generate from. 

14 

15 A PropertyPattern tracks values of a specific property across a sequence, 

16 and can be used to verify patterns or predict next values. 

17 """ 

18 

19 def __init__(self, property_name: str, values: List[Any], is_cyclic: bool = False): 

20 """ 

21 Initialize a property pattern. 

22 

23 Args: 

24 property_name: The name of the property to track 

25 values: The expected sequence of values for this property 

26 is_cyclic: Whether the pattern repeats cyclically 

27 """ 

28 self.property_name = property_name 

29 self.values = values 

30 self.is_cyclic = is_cyclic 

31 

32 def _get_property_value(self, obj: Any) -> Any: 

33 """Get property value from either AbstractObject or dict.""" 

34 if hasattr(obj, "properties"): 

35 return obj.properties.get(self.property_name) 

36 elif hasattr(obj, "__getitem__"): 

37 try: 

38 return obj[self.property_name] 

39 except (KeyError, TypeError): 

40 return None 

41 return getattr(obj, self.property_name, None) 

42 

43 def matches(self, sequence: List[Dict[str, Any]], start_idx: int = 0) -> bool: 

44 """Check if the sequence matches the pattern starting from start_idx.""" 

45 if not sequence: 

46 return True 

47 

48 # For cyclic patterns, we need to check each position relative to the pattern start 

49 if self.is_cyclic: 

50 # Special case for single-value patterns 

51 if len(self.values) == 1: 

52 expected_value = self.values[0] 

53 return all( 

54 self._get_property_value(obj) == expected_value 

55 for obj in sequence[start_idx:] 

56 ) 

57 

58 # Check each position against the pattern 

59 pattern_pos = 0 

60 for i in range(start_idx, len(sequence)): 

61 obj = sequence[i] 

62 expected_value = self.values[pattern_pos] 

63 actual_value = self._get_property_value(obj) 

64 

65 if actual_value != expected_value: 

66 return False 

67 

68 # Move to next position in pattern, wrapping around if needed 

69 pattern_pos = (pattern_pos + 1) % len(self.values) 

70 

71 return True 

72 else: 

73 # Non-cyclic pattern: must match exactly 

74 pattern_length = len(self.values) 

75 remaining_length = len(sequence) - start_idx 

76 

77 # Pattern is longer than remaining sequence 

78 if pattern_length > remaining_length: 

79 return False 

80 

81 # Check each position against the pattern 

82 for i in range(pattern_length): 

83 obj = sequence[start_idx + i] 

84 expected_value = self.values[i] 

85 actual_value = self._get_property_value(obj) 

86 

87 if actual_value != expected_value: 

88 return False 

89 

90 return True 

91 

92 def get_next_value(self, sequence: List[Dict[str, Any]]) -> Any: 

93 """Predict the next value based on the pattern and current sequence.""" 

94 if not self.values: 

95 return None 

96 

97 # For single-value patterns, always return that value 

98 if len(self.values) == 1: 

99 return self.values[0] 

100 

101 # For cyclic patterns, calculate the next position 

102 if self.is_cyclic: 

103 next_pos = len(sequence) % len(self.values) 

104 return self.values[next_pos] 

105 

106 # For non-cyclic patterns, if we've reached the end, return None 

107 matched_length = min(len(sequence), len(self.values)) 

108 if matched_length >= len(self.values): 

109 return None 

110 

111 # Otherwise, return the next value in the pattern 

112 return self.values[matched_length]