| 4 | 1 from enum import Enum | 
|  | 2 import utils.general_utils as utils | 
|  | 3 from typing import List, Union, Optional | 
|  | 4 | 
|  | 5 class RuleErr(utils.CustomErr): | 
|  | 6     """ | 
|  | 7     CustomErr subclass for rule syntax errors. | 
|  | 8     """ | 
|  | 9     errName = "Rule Syntax Error" | 
|  | 10     def __init__(self, rule :str, msg = "no further details provided") -> None: | 
|  | 11         super().__init__( | 
|  | 12             f"rule \"{rule}\" is malformed, {msg}", | 
|  | 13             "please verify your input follows the validity guidelines") | 
|  | 14 | 
|  | 15 class RuleOp(Enum): | 
|  | 16     """ | 
|  | 17     Encodes all operators valid in gene rules. | 
|  | 18     """ | 
|  | 19     OR  = "or" | 
|  | 20     AND = "and" | 
|  | 21 | 
|  | 22     @classmethod | 
|  | 23     def isOperator(cls, op :str) -> bool: | 
|  | 24         return op.upper() in cls.__members__ | 
|  | 25 | 
|  | 26     def __str__(self) -> str: return self.value | 
|  | 27 | 
|  | 28 class OpList(List[Union[str, "OpList"]]): | 
|  | 29     """ | 
|  | 30     Represents a parsed rule and each of its nesting levels, including the operator that level uses. | 
|  | 31     """ | 
|  | 32     def __init__(self, op :Optional[RuleOp] = None) -> None: | 
|  | 33         """ | 
|  | 34         (Private) Initializes an instance of OpList. | 
|  | 35 | 
|  | 36         Args: | 
|  | 37             op (str): Operator to be assigned to the OpList. Defaults to "". | 
|  | 38 | 
|  | 39         Returns: | 
|  | 40             None : practically, an OpList instance. | 
|  | 41         """ | 
|  | 42         self.op = op | 
|  | 43 | 
|  | 44     def setOpIfMissing(self, op :RuleOp) -> None: | 
|  | 45         """ | 
|  | 46         Sets the operator of the OpList if it's missing. | 
|  | 47 | 
|  | 48         Args: | 
|  | 49             op (str): Operator to be assigned to the OpList. | 
|  | 50 | 
|  | 51         Returns: | 
|  | 52             None | 
|  | 53         """ | 
|  | 54         if not self.op: self.op = op | 
|  | 55 | 
|  | 56     def __repr__(self, indent = "") -> str: | 
|  | 57         """ | 
|  | 58         (Private) Returns a string representation of the current OpList instance. | 
|  | 59 | 
|  | 60         Args: | 
|  | 61             indent (str): Indentation level . Defaults to "". | 
|  | 62 | 
|  | 63         Returns: | 
|  | 64             str: A string representation of the current OpList instance. | 
|  | 65         """ | 
|  | 66         nextIndent = indent + "  " | 
|  | 67         return f"<{self.op}>[\n" + ",\n".join([ | 
|  | 68             f"{nextIndent}{item.__repr__(nextIndent) if isinstance(item, OpList) else item}" | 
|  | 69             for item in self ]) + f"\n{indent}]" | 
|  | 70 | 
|  | 71 class RuleStack: | 
|  | 72     """ | 
|  | 73     FILO stack structure to save the intermediate representation of a Rule during parsing, with the | 
|  | 74     current nesting level at the top of the stack. | 
|  | 75     """ | 
|  | 76     def __init__(self) -> None: | 
|  | 77         """ | 
|  | 78         (Private) initializes an instance of RuleStack. | 
|  | 79 | 
|  | 80         Returns: | 
|  | 81             None : practically, a RuleStack instance. | 
|  | 82         """ | 
|  | 83         self.__stack = [OpList()] # the stack starts out with the result list already allocated | 
|  | 84         self.__updateCurrent() | 
|  | 85 | 
|  | 86     def pop(self) -> None: | 
|  | 87         """ | 
|  | 88         Removes the OpList on top of the stack, also flattening it once when possible. | 
|  | 89 | 
|  | 90         Side Effects: | 
|  | 91             self : mut | 
|  | 92 | 
|  | 93         Returns: | 
|  | 94             None | 
|  | 95         """ | 
|  | 96         oldTop = self.__stack.pop() | 
|  | 97         if len(oldTop) == 1 and isinstance(oldTop[0], OpList): self.__stack[-1][-1] = oldTop[0] | 
|  | 98         self.__updateCurrent() | 
|  | 99 | 
|  | 100     def push(self, operator = "") -> None: | 
|  | 101         """ | 
|  | 102         Adds a new nesting level, in the form of a new OpList on top of the stack. | 
|  | 103 | 
|  | 104         Args: | 
|  | 105             operator : the operator assigned to the new OpList. | 
|  | 106 | 
|  | 107         Side Effects: | 
|  | 108             self : mut | 
|  | 109 | 
|  | 110         Returns: | 
|  | 111             None | 
|  | 112         """ | 
|  | 113         newLevel = OpList(operator) | 
|  | 114         self.current.append(newLevel) | 
|  | 115         self.__stack.append(newLevel) | 
|  | 116         self.__updateCurrent() | 
|  | 117 | 
|  | 118     def popForward(self) -> None: | 
|  | 119         """ | 
|  | 120         Moves the last "actual" item from the 2nd to last list to the beginning of the top list, as per | 
|  | 121         the example below: | 
|  | 122         stack  : [list_a, list_b] | 
|  | 123         list_a : [item1, item2, list_b] --> [item1, list_b] | 
|  | 124         list_b : [item3, item4]         --> [item2, item3, item4] | 
|  | 125 | 
|  | 126         This is essentially a "give back as needed" operation. | 
|  | 127 | 
|  | 128         Side Effects: | 
|  | 129             self : mut | 
|  | 130 | 
|  | 131         Returns: | 
|  | 132             None | 
|  | 133         """ | 
|  | 134         self.current.insert(0, self.__stack[-2].pop(-2)) | 
|  | 135 | 
|  | 136     def currentIsAnd(self) -> bool: | 
|  | 137         """ | 
|  | 138         Checks if the current OpList's assigned operator is "and". | 
|  | 139 | 
|  | 140         Returns: | 
|  | 141             bool : True if the current OpList's assigned operator is "and", False otherwise. | 
|  | 142         """ | 
|  | 143         return self.current.op is RuleOp.AND | 
|  | 144 | 
|  | 145     def obtain(self, err :Optional[utils.CustomErr] = None) -> Optional[OpList]: | 
|  | 146         """ | 
|  | 147         Obtains the first OpList on the stack, only if it's the only element. | 
|  | 148 | 
|  | 149         Args: | 
|  | 150             err : The error to raise if obtaining the result is not possible. | 
|  | 151 | 
|  | 152         Side Effects: | 
|  | 153             self : mut | 
|  | 154 | 
|  | 155         Raises: | 
|  | 156             err: If given, otherwise None is returned. | 
|  | 157 | 
|  | 158         Returns: | 
|  | 159             Optional[OpList]: The first OpList on the stack, only if it's the only element. | 
|  | 160         """ | 
|  | 161 | 
|  | 162         if len(self.__stack) == 1: return self.__stack.pop() | 
|  | 163         if err: raise err | 
|  | 164         return None | 
|  | 165 | 
|  | 166     def __updateCurrent(self) -> None: | 
|  | 167         """ | 
|  | 168         (Private) Updates the current OpList to the one on top of the stack. | 
|  | 169 | 
|  | 170         Side Effects: | 
|  | 171             self : mut | 
|  | 172 | 
|  | 173         Returns: | 
|  | 174             None | 
|  | 175         """ | 
|  | 176         self.current = self.__stack[-1] | 
|  | 177 | 
|  | 178 def parseRuleToNestedList(rule :str) -> OpList: | 
|  | 179     """ | 
|  | 180     Parse a single rule from its string representation to an OpList, making all priority explicit | 
|  | 181     through nesting levels. | 
|  | 182 | 
|  | 183     Args: | 
|  | 184         rule : the string representation of a rule to be parsed. | 
|  | 185 | 
|  | 186     Raises: | 
|  | 187         RuleErr : whenever something goes wrong during parsing. | 
|  | 188 | 
|  | 189     Returns: | 
|  | 190         OpList : the parsed rule. | 
|  | 191     """ | 
|  | 192     source = iter(rule | 
|  | 193         .replace("(", "( ").replace(")", " )") # Single out parens as words | 
|  | 194         .strip()  # remove whitespace at extremities | 
|  | 195         .split()) # split by spaces | 
|  | 196 | 
|  | 197     stack = RuleStack() | 
|  | 198     nestingErr = RuleErr(rule, "mismatch between open and closed parentheses") | 
|  | 199     try: | 
|  | 200         while True: # keep reading until source ends | 
|  | 201             while True: | 
|  | 202                 operand = next(source, None) # expected name or rule opening | 
|  | 203                 if operand is None: raise RuleErr(rule, "found trailing open parentheses") | 
|  | 204                 if operand == "and" or operand == "or" or operand == ")": # found operator instead, panic | 
|  | 205                     raise RuleErr(rule, f"found \"{operand}\" in unexpected position") | 
|  | 206 | 
|  | 207                 if operand != "(": break # found name | 
|  | 208 | 
|  | 209                 # found rule opening, we add new nesting level but don't know the operator | 
|  | 210                 stack.push() | 
|  | 211 | 
|  | 212             stack.current.append(operand) | 
|  | 213 | 
|  | 214             while True: # keep reading until operator is found or source ends | 
|  | 215                 operator = next(source, None) # expected operator or rule closing | 
|  | 216                 if operator and operator != ")": break # found operator | 
|  | 217 | 
|  | 218                 if stack.currentIsAnd(): stack.pop() # we close the "and" chain | 
|  | 219 | 
|  | 220                 if not operator: break | 
|  | 221                 stack.pop() # we close the parentheses | 
|  | 222 | 
|  | 223             # we proceed with operator: | 
|  | 224             if not operator: break # there is no such thing as a double loop break.. yet | 
|  | 225 | 
|  | 226             if not RuleOp.isOperator(operator): raise RuleErr( | 
|  | 227                 rule, f"found \"{operator}\" in unexpected position, expected operator") | 
|  | 228 | 
|  | 229             operator = RuleOp(operator) | 
|  | 230             if operator is RuleOp.OR and stack.currentIsAnd(): | 
|  | 231                 stack.pop() | 
|  | 232 | 
|  | 233             elif operator is RuleOp.AND and not stack.currentIsAnd(): | 
|  | 234                 stack.push(operator) | 
|  | 235                 stack.popForward() | 
|  | 236 | 
|  | 237             stack.current.setOpIfMissing(operator) # buffer now knows what operator its data had | 
|  | 238 | 
|  | 239     except RuleErr as err: raise err # bubble up proper errors | 
|  | 240     except: raise nestingErr # everything else is interpreted as a nesting error. | 
|  | 241 | 
|  | 242     parsedRule = stack.obtain(nestingErr) | 
|  | 243     return parsedRule[0] if len(parsedRule) == 1 and isinstance(parsedRule[0], list) else parsedRule |