template.py (10318B)
1 import copy 2 import re 3 4 from plint import error, rhyme 5 from plint.common import normalize 6 from plint.nature import nature_count 7 from plint.options import default_options 8 from plint.pattern import Pattern 9 from plint.verse import Verse 10 11 12 OPTION_ALIASES = { 13 'fusionner': 'merge', 14 'ambiguous_ok': 'forbidden_ok', 15 'ambigu_ok': 'forbidden_ok', 16 'dierese': 'diaeresis', 17 'verifie_occurrences': 'check_occurrences', 18 'repetition_ok': 'repeat_ok', 19 'incomplet_ok': 'incomplete_ok', 20 'phon_supposee_ok': 'phon_supposed_ok', 21 'oeil_supposee_ok': 'eye_supposed_ok', 22 'oeil_tolerance_ok': 'eye_tolerance_ok', 23 'pauvre_oeil_requise': 'poor_eye_required', 24 'pauvre_oeil_supposee_ok': 'poor_eye_supposed_ok', 25 'pauvre_oeil_vocalique_ok': 'poor_eye_vocalic_ok', 26 } 27 28 29 def reset_conditional(d): 30 return dict((k, v) for k, v in d.items() if len(k) > 0 and k[0] == '!') 31 32 33 class Template: 34 35 def __init__(self, template_string=None): 36 self.template = [] 37 self.pattern_line_no = 0 38 self.options = dict(default_options) 39 self.mergers = [] 40 self.old_position = None 41 self.old_env = None 42 self.old_femenv = None 43 self.old_occenv = None 44 self.overflowed = False 45 if template_string is not None: 46 self.load(template_string) 47 self.line_no = 0 48 self.position = 0 49 self.prev = None 50 self.env = {} 51 self.feminine_environment = {} 52 self.occurrence_environment = {} 53 self.reject_errors = False 54 55 def load(self, template_string): 56 """Load from a string""" 57 for line in template_string.split('\n'): 58 line = line.strip() 59 self.pattern_line_no += 1 60 if len(line) != 0 and line[0] != '#': 61 if line[0] == '!': 62 # don't count the '!' in the options, that's why we use [1:] 63 for option_string in line.split()[1:]: 64 self.read_option(option_string) 65 else: 66 self.template.append(self.parse_line(line.strip())) 67 if len(self.template) == 0: 68 raise error.TemplateLoadError("Template is empty") 69 70 def read_option(self, option_string): 71 try: 72 key, value = option_string.split(':') 73 except ValueError: 74 raise error.TemplateLoadError("Global options must be provided as key-value pairs") 75 if key in OPTION_ALIASES: 76 key = OPTION_ALIASES[key] 77 if key == 'merge': 78 self.mergers.append(value) 79 elif key == 'diaeresis': 80 if value == "classique": 81 value = "classical" 82 if value not in ["permissive", "classical"]: 83 raise error.TemplateLoadError("Bad value for global option %s" % key) 84 self.options['diaeresis'] = value 85 elif key in self.options: 86 self.options[key] = str2bool(value) 87 else: 88 raise error.TemplateLoadError("Unknown global option") 89 90 def parse_line(self, line): 91 """Parse template line from a line""" 92 split = line.split(' ') 93 metric = split[0] 94 if len(split) >= 2: 95 my_id = split[1] 96 else: 97 my_id = str(self.pattern_line_no) # unique 98 if len(split) >= 3: 99 feminine_id = split[2] 100 else: 101 feminine_id = str(self.pattern_line_no) # unique 102 id_split = my_id.split(':') 103 classical = True 104 n_common_suffix_phones = 1 105 if len(id_split) >= 2: 106 constraint = id_split[-1].split('|') 107 if len(constraint) > 0: 108 classical = False if constraint[0] in ["no", "non"] else constraint[0] 109 if len(constraint) > 1: 110 n_common_suffix_phones = int(constraint[1]) 111 else: 112 constraint = [] 113 if len(constraint) == 0: 114 n_common_suffix_phones = 1 115 return Pattern(metric, my_id, feminine_id, rhyme.Constraint(classical, n_common_suffix_phones)) 116 117 def match(self, line, output_file=None, last=False, n_syllables=None, offset=0): 118 """Check a line against current pattern, return errors""" 119 120 was_incomplete = last and not self.beyond 121 122 errors = [] 123 pattern = self.get() 124 125 line_with_case = normalize(line, downcase=False) 126 127 verse = Verse(line, self, pattern) 128 129 if n_syllables: 130 verse.print_n_syllables(n_syllables, offset, output_file) 131 return errors, pattern, verse 132 133 if last: 134 if was_incomplete and not self.options['incomplete_ok'] and not self.overflowed: 135 return [error.ErrorIncompleteTemplate()], pattern, verse 136 return [], pattern, verse 137 138 if self.overflowed: 139 return [error.ErrorOverflowedTemplate()], pattern, verse 140 141 rhyme_failed = False 142 # rhymes 143 if pattern.my_id not in self.env: 144 # initialize the rhyme 145 # last_count is passed later 146 self.env[pattern.my_id] = rhyme.Rhyme(verse.normalized, pattern.constraint, self.mergers, self.options) 147 else: 148 # update the rhyme 149 self.env[pattern.my_id].feed(verse.normalized, pattern.constraint) 150 if not self.env[pattern.my_id].satisfied_phon(): 151 # no more possible rhymes, something went wrong, check phon 152 self.env[pattern.my_id].rollback() 153 rhyme_failed = True 154 errors.append(error.ErrorBadRhymeSound(self.env[pattern.my_id], 155 self.env[pattern.my_id].new_rhyme)) 156 157 # occurrences 158 if self.options['check_occurrences']: 159 if pattern.my_id not in self.occurrence_environment.keys(): 160 self.occurrence_environment[pattern.my_id] = {} 161 last_word = re.split(r'[- ]', line_with_case)[-1] 162 if last_word not in self.occurrence_environment[pattern.my_id].keys(): 163 self.occurrence_environment[pattern.my_id][last_word] = 0 164 self.occurrence_environment[pattern.my_id][last_word] += 1 165 if self.occurrence_environment[pattern.my_id][last_word] > nature_count(last_word): 166 errors.insert(0, 167 error.ErrorMultipleWordOccurrence(last_word, 168 self.occurrence_environment[pattern.my_id][last_word])) 169 170 verse.phon = self.env[pattern.my_id].phon 171 verse.parse() 172 173 # now that we have parsed, adjust rhyme to reflect last word length 174 # and check eye 175 if not rhyme_failed: 176 self.env[pattern.my_id].adjust_last_count(verse.get_last_count()) 177 if not self.env[pattern.my_id].satisfied_eye(): 178 old_phon = len(self.env[pattern.my_id].phon) 179 self.env[pattern.my_id].rollback() 180 errors.append(error.ErrorBadRhymeEye(self.env[pattern.my_id], 181 self.env[pattern.my_id].new_rhyme, old_phon)) 182 183 errors = verse.problems() + errors 184 185 verse.print_possible(output_file) 186 187 # rhyme genres 188 # inequality constraint 189 # TODO this is simplistic and order-dependent 190 if pattern.feminine_id.swapcase() in self.feminine_environment.keys(): 191 new = {'M', 'F'} - self.feminine_environment[pattern.feminine_id.swapcase()] 192 if len(new) > 0: 193 self.feminine_environment[pattern.feminine_id] = new 194 if pattern.feminine_id not in self.feminine_environment.keys(): 195 if pattern.feminine_id == 'M': 196 x = {'M'} 197 elif pattern.feminine_id == 'F': 198 x = {'F'} 199 else: 200 x = {'M', 'F'} 201 self.feminine_environment[pattern.feminine_id] = x 202 old = list(self.feminine_environment[pattern.feminine_id]) 203 new = verse.genders() 204 self.feminine_environment[pattern.feminine_id] &= set(new) 205 if len(self.feminine_environment[pattern.feminine_id]) == 0: 206 errors.append(error.ErrorBadRhymeGenre(old, new)) 207 208 return errors, pattern, verse 209 210 def reset_state(self): 211 """Reset our state, except ids starting with '!'""" 212 self.position = 0 213 self.env = reset_conditional(self.env) 214 self.feminine_environment = reset_conditional(self.feminine_environment) 215 self.occurrence_environment = {} # always reset 216 217 @property 218 def beyond(self): 219 return self.position >= len(self.template) 220 221 def get(self): 222 """Get next state, resetting if needed""" 223 self.old_position = self.position 224 self.old_env = copy.deepcopy(self.env) 225 self.old_femenv = copy.deepcopy(self.feminine_environment) 226 self.old_occenv = copy.deepcopy(self.occurrence_environment) 227 if self.beyond: 228 if not self.options['repeat_ok']: 229 self.overflowed = True 230 self.reset_state() 231 result = self.template[self.position] 232 self.position += 1 233 return result 234 235 def back(self): 236 """Revert to previous state""" 237 self.position = self.old_position 238 self.env = copy.deepcopy(self.old_env) 239 self.feminine_environment = copy.deepcopy(self.old_femenv) 240 self.occurrence_environment = copy.deepcopy(self.old_occenv) 241 242 def check(self, line, output_file=None, last=False, n_syllables=None, offset=0): 243 """Check line (wrapper)""" 244 self.line_no += 1 245 line = line.rstrip() 246 if normalize(line) == '' and not last: 247 return None 248 249 errors, pattern, verse = self.match(line, output_file, 250 last=last, n_syllables=n_syllables, offset=offset) 251 if len(errors) > 0 and self.reject_errors: 252 self.back() 253 self.line_no -= 1 254 return error.ErrorCollection(self.line_no, 255 line, pattern, verse, errors) 256 257 258 def str2bool(x): 259 if x.lower() in ["yes", "oui", "y", "o", "true", "t", "vrai", "v"]: 260 return True 261 if x.lower() in ["no", "non", "n", "false", "faux", "f"]: 262 return False 263 raise error.TemplateLoadError("Bad value in global option")