b3fff134cc4a68585be8c5c6c786891f85c26818
[iramuteq] / functions.py
1 #!/bin/env python
2 # -*- coding: utf-8 -*-
3 #Author: Pierre Ratinaud
4 #Copyright (c) 2008-2012 Pierre Ratinaud
5 #Lisense: GNU/GPL
6
7 import wx
8 import re
9 from ConfigParser import ConfigParser
10 from subprocess import Popen, call, PIPE
11 import thread
12 import os
13 import ast
14 import sys
15 import csv
16 import platform
17 import traceback
18 import codecs
19 import locale
20 import datetime
21 from copy import copy
22 from shutil import copyfile
23 #from dialog import BugDialog
24 print 'TEST LOGGING funcion'
25 import logging
26
27 log = logging.getLogger('iramuteq')
28
29
30 indices_simi = [u'cooccurrence' ,'pourcentage de cooccurrence',u'Russel',u'Jaccard', 'Kulczynski1', 'Kulczynski2', 'Mountford', 'Fager', 'simple matching', 'Hamman', 'Faith', 'Tanimoto', 'Dice', 'Phi', 'Stiles', 'Michael', 'Mozley', 'Yule', 'Yule2', 'Ochiai', 'Simpson', 'Braun-Blanquet','Chi-squared', 'Phi-squared', 'Tschuprow', 'Cramer', 'Pearson', 'binomial']
31
32
33 class History :
34     def __init__(self, filein, syscoding = 'utf8') :
35         self.filein = filein
36         self.syscoding = syscoding
37         self.corpora = {}
38         self.openedcorpus = {}
39         self.analyses = {}
40         self.history = {}
41         self.opened = {}
42         self.read()
43
44     def read(self) :
45         self.conf = DoConf(self.filein)
46         self.order = {}
47         self.ordera = {}
48         for i, section in enumerate(self.conf.conf.sections()) :
49             if self.conf.conf.has_option(section, 'corpus_name') :
50                 self.corpora[section] = self.conf.getoptions(section)
51                 self.order[len(self.order)] = section
52             else :
53                 self.analyses[section] = self.conf.getoptions(section)
54                 self.ordera[len(self.ordera)] = section
55         todel = []
56         for corpus in self.corpora :
57             self.history[corpus] = copy(self.corpora[corpus])
58         for analyse in self.analyses :
59             if self.analyses[analyse]['corpus'] in self.corpora :
60                 if 'analyses' in self.history[self.analyses[analyse]['corpus']] :
61                     self.history[self.analyses[analyse]['corpus']]['analyses'].append(self.analyses[analyse])
62                     todel.append(analyse)
63                 else :
64                     self.history[self.analyses[analyse]['corpus']]['analyses'] = [self.analyses[analyse]]
65                     todel.append(analyse)
66             else :
67                  self.history[analyse] = self.analyses[analyse]
68         #for analyse in todel :
69         #    del self.analyses[analyse]
70     
71     def write(self) :
72         sections = self.corpora.keys() + self.analyses.keys()
73         parametres = [self.corpora[key] for key in self.corpora.keys()] + [self.analyses[key] for key in self.analyses.keys()]
74         self.conf.makeoptions(sections, parametres)
75         log.info('write history')
76
77     def add(self, analyse) :
78         if 'corpus' in analyse :
79             acorpus_uuid =  analyse['corpus']
80             if acorpus_uuid in self.corpora :
81                 if 'analyses' in self.history[acorpus_uuid] :
82                     self.history[acorpus_uuid]['analyses'].append(analyse)
83                 else :
84                     self.history[acorpus_uuid]['analyses'] = [analyse]
85                 self.analyses[analyse['uuid']] = analyse
86             else :
87                 self.analyses[analyse['uuid']] = analyse
88         elif 'corpus_name' in analyse :
89             self.history[analyse['uuid']] = analyse
90             self.corpora[analyse['uuid']] = analyse
91         self.write()
92
93     def delete(self, uuid, corpus = False) :
94         if corpus :
95             del self.corpora[uuid]
96             self.conf.conf.remove_section(uuid)
97             for analyse in self.history[uuid].get('analyses', [False]) :
98                 if analyse :
99                     del self.analyses[analyse['uuid']]
100                     self.conf.conf.remove_section(analyse['uuid'])
101         else :
102             del self.analyses[uuid]
103             self.conf.conf.remove_section(uuid)
104         self.write()
105
106     def addtab(self, analyse) :
107         self.opened[analyse['uuid']] = analyse
108
109     def rmtab(self, analyse) :
110         del self.opened[analyse['uuid']]
111     
112     def __str__(self) :
113         return str(self.history)
114
115 class DoConf :
116     def __init__(self, configfile=None, diff = None, parametres = None) :
117         self.configfile = configfile
118         self.conf = ConfigParser()
119         if configfile is not None :
120             self.conf.readfp(codecs.open(configfile, 'r', 'utf8'))
121         self.parametres = {}
122         if parametres is not None :
123             self.doparametres(parametres)
124
125     def doparametres(self, parametres) :
126         return parametres
127
128     def getsections(self) :
129         return self.conf.sections()
130
131     def getoptions(self, section = None, diff = None):
132         parametres = {}
133         if section is None :
134             section = self.conf.sections()[0]
135         for option in self.conf.options(section) :
136             if self.conf.get(section, option).isdigit() :
137                 parametres[option] = int(self.conf.get(section, option))
138             elif self.conf.get(section, option) == 'False' :
139                 parametres[option] = False
140             elif self.conf.get(section, option) == 'True' :
141                 parametres[option] = True
142             elif self.conf.get(section, option).startswith('(') and self.conf.get(section, option).endswith(')') :
143                 parametres[option] = ast.literal_eval(self.conf.get(section, option))
144             else :
145                 parametres[option] = self.conf.get(section, option)
146         if 'type' not in parametres :
147             parametres['type'] = section
148         return parametres
149             
150     def makeoptions(self, sections, parametres, outfile = None) :
151         for i, section in enumerate(sections) :
152             if not self.conf.has_section(section) :
153                 self.conf.add_section(section)
154             for option in parametres[i] :
155                 if isinstance(parametres[i][option], int) :
156                     self.conf.set(section, option, `parametres[i][option]`)
157                 elif isinstance(parametres[i][option], basestring) :
158                     self.conf.set(section, option, parametres[i][option].encode('utf8'))
159                 elif isinstance(parametres[i][option], wx.Colour) :
160                     self.conf.set(section, option, str(parametres[i][option]))
161                 else :
162                     self.conf.set(section, option, `parametres[i][option]`)
163         if outfile is None :
164             outfile = self.configfile
165         with codecs.open(outfile, 'w', 'utf8') as f :
166             self.conf.write(f)
167
168     def totext(self, parametres) :
169         txt = ['Corpus']
170         for val in parametres :
171             if isinstance(parametres[val], int) :
172                 txt.append(' \t\t: '.join([val, `parametres[val]`]))
173             else :
174                 txt.append(' \t\t: '.join([val, parametres[val]]))
175         return '\n'.join(txt)
176
177
178 def write_tab(tab, fileout) :
179         writer = csv.writer(open(fileout, 'wb'), delimiter=';', quoting = csv.QUOTE_NONNUMERIC)
180         writer.writerows(tab)
181
182 class BugDialog(wx.Dialog):
183     def __init__(self, *args, **kwds):
184         # begin wxGlade: MyDialog.__init__
185         kwds["style"] = wx.DEFAULT_DIALOG_STYLE
186         kwds["size"] = wx.Size(500, 200)
187         wx.Dialog.__init__(self, *args, **kwds)
188         self.text_ctrl_1 = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE)
189         self.button_1 = wx.Button(self, wx.ID_OK, "")
190
191         self.__set_properties()
192         self.__do_layout()
193         # end wxGlade
194
195     def __set_properties(self):
196         # begin wxGlade: MyDialog.__set_properties
197         self.SetTitle("Bug")
198         self.SetMinSize(wx.Size(500, 200))
199         self.text_ctrl_1.SetMinSize(wx.Size(500, 200))
200         
201         # end wxGlade
202
203     def __do_layout(self):
204         # begin wxGlade: MyDialog.__do_layout
205         sizer_1 = wx.BoxSizer(wx.VERTICAL)
206         sizer_1.Add(self.text_ctrl_1, 1, wx.EXPAND, 0)
207         sizer_1.Add(self.button_1, 0, wx.ALIGN_CENTER_HORIZONTAL, 0)
208         self.SetSizer(sizer_1)
209         sizer_1.Fit(self)
210         self.Layout()
211
212
213 def CreateIraFile(DictPathOut, clusternb, corpname='corpus_name', section = 'analyse'):
214     AnalyseConf = ConfigParser()
215     AnalyseConf.read(DictPathOut['ira'])
216     AnalyseConf.add_section(section)
217     date = datetime.datetime.now().ctime()
218     AnalyseConf.set(section, 'date', str(date))
219     AnalyseConf.set(section, 'clusternb', clusternb)
220     AnalyseConf.set(section, 'corpus_name', corpname)
221
222     fileout = open(DictPathOut['ira'], 'w')
223     AnalyseConf.write(fileout)
224     fileout.close()
225
226 def sortedby(list, direct, *indices):
227
228     """
229         sortedby: sort a list of lists (e.g. a table) by one or more indices
230                   (columns of the table) and return the sorted list
231
232         e.g.
233          for list = [[2,3],[1,2],[3,1]]:
234          sortedby(list,1) will return [[3, 1], [1, 2], [2, 3]],
235          sortedby(list,0) will return [[1, 2], [2, 3], [3, 1]]
236     """
237
238     nlist = map(lambda x, indices=indices: 
239                  map(lambda i, x=x: x[i], indices) + [x],
240                  list)
241     if direct == 1:
242         nlist.sort()
243     elif direct == 2:
244         nlist.sort(reverse=True)
245     return map(lambda l: l[-1], nlist)
246
247 def add_type(line, dictlem):
248     if line[4] in dictlem:
249         line.append(dictlem[line[4]])
250     else :
251         line.append('')
252     return line
253
254 def treat_line_alceste(i, line) :
255     if line[0] == '*' or line[0] == '*****' :
256         return line + ['']
257     if line[5] == 'NA':
258         print 'NA', line[5]
259         pass
260     elif float(line[5].replace(',', '.')) < 0.0001:
261         line[5] = '< 0,0001'
262     elif float(line[5].replace(',', '.')) > 0.05:
263         line[5] = 'NS (%s)' % str(float(line[5].replace(',', '.')))[0:7]
264     else:
265         line[5] = str(float(line[5].replace(',', '.')))[0:7]
266     return [i, int(line[0]), int(line[1]), float(line[2]), float(line[3]), line[6], line[4], line[5]]
267
268 def ReadProfileAsDico(parent, File, Alceste=False, encoding = sys.getdefaultencoding()):
269     #print 'lecture des profils : ReadProfileAsDico'
270     #if Alceste :
271     #    print 'lecture du dictionnaire de type'
272     #    dictlem = {}
273     #    for line in parent.corpus.lem_type_list :
274     #        dictlem[line[0]] = line[1]
275     dictlem = {}
276     print 'lecture des profiles'
277     #encoding = sys.getdefaultencoding()
278     print encoding
279     FileReader = codecs.open(File, 'r', encoding)
280     Filecontent = FileReader.readlines()
281     FileReader.close()
282     DictProfile = {}
283     count = 0
284     rows = [row.replace('\n', '').replace("'", '').replace('\"', '').replace(',', '.').replace('\r','').split(';') for row in Filecontent]
285     rows.pop(0)
286     ClusterNb = rows[0][2]
287     rows.pop(0)
288     clusters = [row[2] for row in rows if row[0] == u'**']
289     valclusters = [row[1:4] for row in rows if row[0] == u'****']
290     lp = [i for i, line in enumerate(rows) if line[0] == u'****']
291     prof = [rows[lp[i] + 1:lp[i+1] - 1] for i in range(0, len(lp)-1)] + [rows[lp[-1] + 1:len(rows)]] 
292     if Alceste :
293         prof = [[add_type(row, dictlem) for row in pr] for pr in prof]
294         prof = [[treat_line_alceste(i,line) for i, line in enumerate(pr)] for pr in prof] 
295     else :
296         prof = [[line + [''] for line in pr] for pr in prof]
297         prof = [[treat_line_alceste(i,line) for i, line in enumerate(pr)] for pr in prof]
298     for i, cluster in enumerate(clusters):
299         DictProfile[cluster] = [valclusters[i]] + prof[i]
300     return DictProfile
301
302 def GetTxtProfile(dictprofile) :
303     proflist = []
304     for classe in range(0, len(dictprofile)) :
305         prof = dictprofile[str(classe + 1)]
306         clinfo = prof[0]
307         proflist.append('\n'.join([' '.join(['classe %i' % (classe + 1), '-', '%s uce sur %s - %s%%' % (clinfo[0], clinfo[1], clinfo[2])]), '\n'.join(['%5s|%5s|%6s|%6s|%8s|%8s|%20s\t%10s' % tuple([str(val) for val in line]) for line in prof if len(line)==8])]))
308     return '\n\n'.join(proflist)
309
310 def formatExceptionInfo(maxTBlevel=5):
311          cla, exc, trbk = sys.exc_info()
312          excName = cla.__name__
313          try:
314              excArgs = exc.__dict__["args"]
315          except KeyError:
316              excArgs = "<no args>"
317          excTb = traceback.format_tb(trbk, maxTBlevel)
318          return (excName, excArgs, excTb)
319
320
321 #fonction des etudiants de l'iut
322 def decoupercharact(chaine, longueur, longueurOptimale, separateurs = None) :
323     """
324         on part du dernier caractère, et on recule jusqu'au début de la chaîne.
325         Si on trouve un '$', c'est fini.
326         Sinon, on cherche le meilleur candidat. C'est-à-dire le rapport poids/distance le plus important.
327     """
328     separateurs = [[u'.', 60.0], [u'?', 60.0], [u'!', 60.0], [u'£', 60], [u':', 50.0], [u';', 40.0], [u',', 10.0], [u' ', 0.1]]
329     trouve = False                 # si on a trouvé un bon séparateur
330     iDecoupe = 0                # indice du caractere ou il faut decouper
331     
332     # on découpe la chaine pour avoir au maximum 240 caractères
333     longueur = min(longueur, len(chaine) - 1)
334     chaineTravail = chaine[:longueur + 1]
335     nbCar = longueur
336     meilleur = ['', 0, 0]        # type, poids et position du meilleur separateur
337     
338     # on vérifie si on ne trouve pas un '$'
339     indice = chaineTravail.find(u'$')
340     if indice > -1:
341         trouve = True
342         iDecoupe = indice
343
344     # si on ne trouve rien, on cherche le meilleur séparateur
345     if not trouve:
346         while nbCar >= 0:
347             caractere = chaineTravail[nbCar]
348             distance = abs(longueurOptimale - nbCar) + 1
349             meilleureDistance = abs(longueurOptimale - meilleur[2]) + 1
350
351             # on vérifie si le caractére courant est une marque de ponctuation
352             for s in separateurs:
353                 if caractere == s[0]:
354                     # si c'est une ponctuation 
355                     
356                     if s[1] / distance > float(meilleur[1]) / meilleureDistance:
357                         # print nbCar, s[0]
358                         meilleur[0] = s[0]
359                         meilleur[1] = s[1]
360                         meilleur[2] = nbCar
361                         trouve = True
362                         iDecoupe = nbCar
363                         
364                     # et on termine la recherche
365                     break
366
367             # on passe au caractère précédant
368             nbCar = nbCar - 1
369     
370     # si on a trouvé
371     if trouve:
372         fin = chaine[iDecoupe + 1:]
373         retour = chaineTravail[:iDecoupe]
374         return len(retour) > 0, retour.split(), fin
375     # si on a rien trouvé
376     return False, chaine.split(), ''
377
378 def BugReport(parent):
379     for ch in parent.GetChildren():
380         if "<class 'wx._windows.ProgressDialog'>" == str(type(ch)):
381             ch.Destroy()   
382     dial = BugDialog(parent)
383     txt = u'            !== BUG ==!       \n'
384     txt += u'*************************************\n'
385     for line in formatExceptionInfo():
386         if type(line) == type([]):
387             for don in line:
388                 txt += don.replace('    ', ' ')
389         else:
390             txt += line + '\n'
391     if 'Rerror' in dir(parent) :
392         txt += parent.Rerror
393         parent.Rerror = ''
394     print formatExceptionInfo()
395     log.error(txt)
396     dial.text_ctrl_1.write(txt)
397     dial.CenterOnParent()
398     dial.ShowModal()
399     raise NameError('Bug')
400     
401 def PlaySound(parent):
402     if parent.pref.getboolean('iramuteq', 'sound') :
403         try:
404             if "gtk2" in wx.PlatformInfo:
405                 error = Popen(['aplay','-q',os.path.join(parent.AppliPath,'son_fin.wav')])
406             else :    
407                 sound = wx.Sound(os.path.join(parent.AppliPath, 'son_fin.wav'))
408                 sound.Play(wx.SOUND_SYNC)
409         except :
410             print 'pas de son'
411
412 def ReadDicoAsDico(dicopath):
413     with codecs.open(dicopath, 'r', 'UTF8') as f:
414         content = f.readlines()
415     dico = {}
416     for line in content :
417         if line[0] != u'':
418             line = line.replace(u'\n', '').replace('"', '').split('\t')
419             dico[line[0]] = line[1:]
420     return dico
421
422 def ReadLexique(parent, lang = 'french'):
423     parent.lexique = ReadDicoAsDico(parent.DictPath.get(lang, 'french'))
424
425 def ReadList(filein, encoding = sys.getdefaultencoding()):
426     #file = open(filein)
427     file = codecs.open(filein, 'r', encoding)
428     content = file.readlines()
429     file.close()
430     first = content.pop(0)
431     first = first.replace('\n', '').replace('\r','').replace('\"', '').split(';')
432     dict = {}
433     i = 0
434     for line in content:
435         line = line.replace('\n', '').replace('\r','').replace('\"', '').replace(',', '.')
436         line = line.split(';')
437         nline = [line[0]]
438         for val in line[1:]:
439             if val == u'NA' :
440                 don = ''
441             else: 
442                 try:
443                     don = int(val)
444                 except:
445                     don = float('%.5f' % float(val))
446             nline.append(don)
447         dict[i] = nline
448         i += 1
449     return dict, first
450
451 def exec_rcode(rpath, rcode, wait = True, graph = False):
452     print rpath, rcode
453     needX11 = False
454     if sys.platform == 'darwin' :
455         try :
456             macversion = platform.mac_ver()[0].split('.')
457             print macversion
458             if int(macversion[1]) < 5 :
459                 needX11 = True
460             else :
461                 needX11 = False
462         except :
463             needX11 = False
464
465     rpath = rpath.replace('\\','\\\\')
466     if not graph :
467         if wait :
468             if sys.platform == 'win32':
469                 error = call(["%s" % rpath, "--vanilla","--slave","-f", "%s" % rcode])
470             else :
471                 error = call([rpath, '--vanilla','--slave',"-f %s" % rcode])
472             return error
473         else :
474             if sys.platform == 'win32':
475                 pid = Popen(["%s" % rpath, '--vanilla','--slave','-f', "%s" % rcode])
476             else :
477                 pid = Popen([rpath, '--vanilla','--slave',"-f %s" % rcode], stderr = PIPE)
478             return pid
479     else :
480         if wait :
481             if sys.platform == 'win32':
482                 error = call(["%s" % rpath, '--vanilla','--slave','-f', "%s" % rcode])
483             elif sys.platform == 'darwin' and needX11:
484                 os.environ['DISPLAY'] = ':0.0'
485                 error = call([rpath, '--vanilla','--slave',"-f %s" % rcode])
486             else :
487                 error = call([rpath, '--vanilla','--slave',"-f %s" % rcode])
488             return error
489         else :
490             if sys.platform == 'win32':
491                 pid = Popen(["%s" % rpath, '--vanilla','--slave','-f', "%s" % rcode])
492             elif sys.platform == 'darwin' and needX11:
493                 os.environ['DISPLAY'] = ':0.0'
494                 pid = Popen([rpath, '--vanilla','--slave',"-f %s" % rcode], stderr = PIPE)
495             else :
496                 pid = Popen([rpath, '--vanilla','--slave',"-f %s" % rcode], stderr = PIPE)
497             return pid
498
499 def check_Rresult(parent, pid) :
500     if isinstance(pid, Popen) :
501         if pid.returncode != 0 :
502             error = pid.communicate()
503             error = [str(error[0]), error[1]]
504             if error[1] is None :
505                 error[1] = 'None'
506             parent.Rerror = '\n'.join([str(pid.returncode), '\n'.join(error)])
507             try :
508                 raise Exception('\n'.join(u'Erreur R', '\n'.join(error[1:])))
509             except :
510                 BugReport(parent)
511     else :
512         if pid !=0 :
513             try :
514                 raise Exception(u'Erreur R')
515             except :
516                 BugReport(parent)
517
518 def print_liste(filename,liste):
519     with open(filename,'w') as f :
520         for graph in liste :
521             f.write(';'.join(graph)+'\n')
522
523 def read_list_file(filename, encoding = sys.getdefaultencoding()):
524     with codecs.open(filename,'rU', encoding) as f :
525         content=f.readlines()
526         ncontent=[line.replace('\n','').split(';') for line in content if line.strip() != '']
527     return ncontent
528         
529 class MessageImage(wx.Frame):
530     def __init__(self, *args, **kwds):
531         # begin wxGlade: MyFrame.__init__
532         kwds["style"] = wx.DEFAULT_FRAME_STYLE
533         wx.Frame.__init__(self, *args, **kwds)
534         #self.text_ctrl_1 = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE)
535         self.imageFile = False
536         self.imagename = u"chi_classe.png"
537         self.HtmlPage = wx.html.HtmlWindow(self, -1)
538         if "gtk2" in wx.PlatformInfo:
539             self.HtmlPage.SetStandardFonts()
540         self.HtmlPage.SetFonts('Courier', 'Courier')
541         
542         self.button_1 = wx.Button(self, -1, u"Fermer")
543         self.Bind(wx.EVT_BUTTON, self.OnCloseMe, self.button_1)
544         self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
545         self.__do_layout()
546         # end wxGlade
547
548     def __do_layout(self):
549         # begin wxGlade: MyFrame.__do_layout
550         sizer_1 = wx.BoxSizer(wx.VERTICAL)
551         self.sizer_2 = wx.BoxSizer(wx.VERTICAL)
552         self.sizer_2.Add(self.HtmlPage, 1, wx.EXPAND | wx.ADJUST_MINSIZE, 0)
553         self.sizer_2.Add(self.button_1, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ADJUST_MINSIZE, 0)
554         sizer_1.Add(self.sizer_2, 1, wx.EXPAND, 0)
555         self.SetAutoLayout(True)
556         self.SetSizer(sizer_1)
557         # end wxGlade
558
559     def addsaveimage(self, imageFile) :
560         self.imageFile = imageFile
561         self.button_2 = wx.Button(self, -1, u"Enregistrer l'image...")
562         self.Bind(wx.EVT_BUTTON, self.OnSaveImage, self.button_2)
563         self.sizer_2.Add(self.button_2, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ADJUST_MINSIZE, 0)
564         self.Layout()
565
566     def OnCloseMe(self, event):
567         self.Close(True)
568
569     def OnCloseWindow(self, event):
570         self.Destroy()
571
572     def OnSaveImage(self, event) :
573         dlg = wx.FileDialog(
574             self, message="Enregistrer sous...", defaultDir=os.getcwd(),
575             defaultFile= self.imagename, wildcard="png|*.png", style=wx.SAVE | wx.OVERWRITE_PROMPT
576             )
577         dlg.SetFilterIndex(2)
578         dlg.CenterOnParent()
579         if dlg.ShowModal() == wx.ID_OK:
580             path = dlg.GetPath()
581             copyfile(self.imageFile, path)
582             
583
584 def progressbar(self, maxi) :
585     if 'parent' in dir(self) :
586         parent = self.parent
587     else :
588         parent = self
589     return wx.ProgressDialog("Traitements",
590                              "Veuillez patienter...",
591                              maximum=maxi,
592                              parent=parent,
593                              style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE | wx.PD_ELAPSED_TIME | wx.PD_CAN_ABORT
594                              )
595
596
597 def treat_var_mod(variables) :
598     var_mod = {}
599     for variable in variables :
600         if u'_' in variable :
601             forme = variable.split(u'_')
602             var = forme[0]
603             mod = forme[1]
604             if not var in var_mod :
605                 var_mod[var] = [variable]
606             else :
607                 if not mod in var_mod[var] :
608                     var_mod[var].append(variable)
609     return var_mod