plint

French poetry validator
git clone https://a3nm.net/git/plint/
Log | Files | Refs | README

template.py (9613B)


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