Salome HOME
version 19 mars
[tools/eficas.git] / Noyau / N_JDC.py
1 # coding=utf-8
2 # Copyright (C) 2007-2013   EDF R&D
3 #
4 # This library is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU Lesser General Public
6 # License as published by the Free Software Foundation; either
7 # version 2.1 of the License.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 # Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public
15 # License along with this library; if not, write to the Free Software
16 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17 #
18 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
19
20
21 """
22    Ce module contient la classe JDC qui sert à interpréter un jeu de commandes
23 """
24
25 # Modules Python
26 import os
27 import string
28 import traceback
29 import types
30 import sys
31 import linecache
32
33 # Modules EFICAS
34 import N_OBJECT
35 import N_CR
36 from N_Exception import AsException, InterruptParsingError
37 from N_ASSD import ASSD
38 from N_info import message, SUPERV
39 from strfunc import get_encoding
40 try :
41   from Extensions.i18n import tr
42 except :
43   def tr(txt):
44     return txt
45
46 MemoryErrorMsg = """MemoryError :
47
48 En général, cette erreur se produit car la mémoire utilisée hors du fortran
49 (jeveux) est importante.
50
51 Causes possibles :
52    - le calcul produit de gros objets Python dans une macro-commande ou
53      dans le jeu de commande lui-même,
54    - le calcul appelle un solveur (MUMPS par exemple) ou un outil externe
55      qui a besoin de mémoire hors jeveux,
56    - utilisation de jeveux dynamique,
57    - ...
58
59 Solution :
60    - distinguer la mémoire limite du calcul (case "Mémoire totale" de astk)
61      de la mémoire réservée à jeveux (case "dont Aster"), le reste étant
62      disponible pour les allocations dynamiques.
63 """
64
65
66 class JDC(N_OBJECT.OBJECT):
67
68     """
69        Cette classe interprete un jeu de commandes fourni sous
70        la forme d'une chaine de caractères
71
72        Attributs de classe :
73
74        Attributs d'instance :
75
76     """
77     nature = "JDC"
78     CR = N_CR.CR
79     exec_init = """
80 import Accas
81 from Accas import _F
82 from Accas import *
83 NONE = None
84 """
85
86     from N_utils import SEP
87
88     def __init__(self, definition=None, procedure=None, cata=None,
89                  cata_ord_dico=None, parent=None,
90                  nom='SansNom', appli=None, context_ini=None, **args):
91         self.procedure = procedure
92         self.definition = definition
93         self.cata = cata
94         if type(self.cata) != types.TupleType and cata != None:
95             self.cata = (self.cata,)
96         self._build_reserved_kw_list()
97         self.cata_ordonne_dico = cata_ord_dico
98         self.nom = nom
99         self.appli = appli
100         self.parent = parent
101         self.context_ini = context_ini
102         # On conserve les arguments supplémentaires. Il est possible de passer
103         # des informations globales au JDC par ce moyen. Il pourrait etre plus
104         # sur de mettre en place le mecanisme des mots-cles pour verifier la
105         # validité des valeurs passées.
106         # Ceci reste à faire
107         # On initialise avec les parametres de la definition puis on
108         # update avec ceux du JDC
109         self.args = self.definition.args
110         self.args.update(args)
111         self.nstep = 0
112         self.nsd = 0
113         self.par_lot = 'OUI'
114         self.par_lot_user = None
115         if definition:
116             self.regles = definition.regles
117             self.code = definition.code
118         else:
119             self.regles = ()
120             self.code = "CODE"
121         #
122         #  Creation de l objet compte rendu pour collecte des erreurs
123         #
124         self.cr = self.CR(debut="CR phase d'initialisation",
125                           fin="fin CR phase d'initialisation")
126         # on met le jdc lui-meme dans le context global pour l'avoir sous
127         # l'etiquette "jdc" dans le fichier de commandes
128         self.g_context = {'jdc': self}
129         # message.debug(SUPERV, "g_context : %s - %s", self.g_context, id(self.g_context))
130         # Dictionnaire pour stocker tous les concepts du JDC (acces rapide par
131         # le nom)
132         self.sds_dict = {}
133         self.etapes = []
134         self.index_etapes = {}
135         self.mc_globaux = {}
136         self.current_context = {}
137         self.condition_context = {}
138         self.index_etape_courante = 0
139         self.UserError = "UserError"
140         self.alea = None
141         # permet transitoirement de conserver la liste des étapes
142         self.hist_etape = False
143
144     def compile(self):
145         """
146            Cette methode compile la chaine procedure
147            Si des erreurs se produisent, elles sont consignées dans le
148            compte-rendu self.cr
149         """
150         try:
151             if self.appli != None:
152                 self.appli.affiche_infos(
153                     'Compilation du fichier de commandes en cours ...')
154             # Python 2.7 compile function does not accept unicode filename, so we encode it
155             # with the current locale encoding in order to have a correct
156             # traceback
157             encoded_filename = self.nom.encode(get_encoding())
158             self.proc_compile = compile(
159                 self.procedure, encoded_filename, 'exec')
160         except SyntaxError, e:
161             if CONTEXT.debug:
162                 traceback.print_exc()
163             l = traceback.format_exception_only(SyntaxError, e)
164             self.cr.exception("Compilation impossible : " + string.join(l))
165         except MemoryError, e:
166             self.cr.exception(MemoryErrorMsg)
167         except SystemError, e:
168             erreurs_connues = """
169 Causes possibles :
170  - offset too large : liste trop longue derrière un mot-clé.
171    Solution : liste = (valeurs, ..., )
172               MOT_CLE = *liste,
173 """
174             l = traceback.format_exception_only(SystemError, e)
175             l.append(erreurs_connues)
176             self.cr.exception("Compilation impossible : " + ''.join(l))
177         return
178
179     def exec_compile(self):
180         """
181            Cette méthode execute le jeu de commandes compilé dans le contexte
182            self.g_context de l'objet JDC
183         """
184         CONTEXT.set_current_step(self)
185         # Le module nommage utilise le module linecache pour accéder
186         # au source des commandes du jeu de commandes.
187         # Dans le cas d'un fichier, on accède au contenu de ce fichier
188         # Dans le cas d'une chaine de caractères il faut accéder
189         # aux commandes qui sont dans la chaine
190         import linecache
191         linecache.cache[self.nom] = 0, 0, string.split(
192             self.procedure, '\n'), self.nom
193         try:
194             exec self.exec_init in self.g_context
195             # message.debug(SUPERV, "JDC.exec_compile_1 - len(g_context) = %d",
196             # len(self.g_context.keys()))
197             for obj_cata in self.cata:
198                 if type(obj_cata) == types.ModuleType:
199                     init2 = "from " + obj_cata.__name__ + " import *"
200                     exec init2 in self.g_context
201             # message.debug(SUPERV, "JDC.exec_compile_2 - len(g_context) = %d",
202             # len(self.g_context.keys()))
203
204             # Initialisation du contexte global pour l'évaluation des conditions de BLOC
205             # On utilise une copie de l'initialisation du contexte du jdc
206             self.condition_context = self.g_context.copy()
207
208             # Si l'attribut context_ini n'est pas vide, on ajoute au contexte global
209             # le contexte initial (--> permet d'évaluer un JDC en récupérant un contexte
210             # d'un autre par exemple)
211             if self.context_ini:
212                 self.g_context.update(self.context_ini)
213                 # Update du dictionnaire des concepts
214                 for sdnom, sd in self.context_ini.items():
215                     if isinstance(sd, ASSD):
216                         self.sds_dict[sdnom] = sd
217
218             if self.appli != None:
219                 self.appli.affiche_infos(
220                     'Interprétation du fichier de commandes en cours ...')
221             # On sauve le contexte pour garder la memoire des constantes
222             # En mode edition (EFICAS) ou lors des verifications le contexte
223             # est recalculé
224             # mais les constantes sont perdues
225             self.const_context = self.g_context
226             # message.debug(SUPERV, "pass")
227             exec self.proc_compile in self.g_context
228             # message.debug(SUPERV, "JDC.exec_compile_3 - len(g_context) = %d",
229             # len(self.g_context.keys()))
230
231             CONTEXT.unset_current_step()
232             if self.appli != None:
233                 self.appli.affiche_infos('')
234
235         except InterruptParsingError:
236             # interrupt the command file parsing used by FIN to ignore the end
237             # of the file
238             pass
239
240         except EOFError:
241             # Exception utilise pour interrompre un jeu
242             # de commandes avant la fin
243             # Fonctionnement normal, ne doit pas etre considere comme une
244             # erreur
245             CONTEXT.unset_current_step()
246             self.affiche_fin_exec()
247             self.traiter_fin_exec('commande')
248
249         except AsException, e:
250             # une erreur a ete identifiee
251             if CONTEXT.debug:
252                 traceback.print_exc()
253             # l'exception a été récupérée avant (où, comment ?),
254             # donc on cherche dans le texte
255             txt = str(e)
256             if txt.find('MemoryError') >= 0:
257                 txt = MemoryErrorMsg
258             self.cr.exception(txt)
259             CONTEXT.unset_current_step()
260
261         except NameError, e:
262             etype, value, tb = sys.exc_info()
263             l = traceback.extract_tb(tb)
264             s = traceback.format_exception_only("Erreur de nom", e)[0][:-1]
265             msg = "erreur de syntaxe,  %s ligne %d" % (s, l[-1][1])
266             if CONTEXT.debug:
267                 traceback.print_exc()
268             self.cr.exception(msg)
269             CONTEXT.unset_current_step()
270
271         except self.UserError, exc_val:
272             self.traiter_user_exception(exc_val)
273             CONTEXT.unset_current_step()
274             self.affiche_fin_exec()
275             self.traiter_fin_exec('commande')
276
277         except:
278             # erreur inattendue
279             # sys_exc_typ,sys_exc_value,sys_exc_frame = sys_exc.info()
280             # (tuple de 3 éléments)
281             if CONTEXT.debug:
282                 traceback.print_exc()
283
284             traceback.print_exc()
285
286             exc_typ, exc_val, exc_fr = sys.exc_info()
287             l = traceback.format_exception(exc_typ, exc_val, exc_fr)
288             self.cr.exception(
289                 "erreur non prevue et non traitee prevenir la maintenance " + '\n' + string.join(l))
290             del exc_typ, exc_val, exc_fr
291             CONTEXT.unset_current_step()
292
293     def affiche_fin_exec(self):
294         """
295            Cette methode realise l'affichage final des statistiques de temps
296            apres l'execution de toutes
297            les commandes en mode commande par commande ou par lot
298            Elle doit etre surchargee pour en introduire un
299         """
300         return
301
302     def traiter_fin_exec(self, mode, etape=None):
303         """
304            Cette methode realise un traitement final apres l'execution de toutes
305            les commandes en mode commande par commande ou par lot
306            Par defaut il n'y a pas de traitement. Elle doit etre surchargee
307            pour en introduire un
308         """
309         message.info(SUPERV, "FIN D'EXECUTION %s %s", mode, etape)
310
311     def traiter_user_exception(self, exc_val):
312         """Cette methode realise un traitement sur les exceptions utilisateur
313            Par defaut il n'y a pas de traitement. La méthode doit etre
314            surchargée pour en introduire un.
315         """
316         return
317
318     def register(self, etape):
319         """
320            Cette méthode ajoute etape dans la liste des etapes : self.etapes
321            et retourne un numéro d'enregistrement
322         """
323         self.etapes.append(etape)
324         self.index_etapes[etape] = len(self.etapes) - 1
325         # message.debug(SUPERV, "#%d %s", self.index_etapes[etape], etape.nom)
326         return self.g_register(etape)
327
328     def o_register(self, sd):
329         """
330            Retourne un identificateur pour concept
331         """
332         self.nsd = self.nsd + 1
333         nom = sd.idracine + self.SEP + `self.nsd`
334         return nom
335
336     def g_register(self, etape):
337         """
338             Retourne un identificateur pour etape
339         """
340         self.nstep = self.nstep + 1
341         idetape = etape.idracine + self.SEP + `self.nstep`
342         return idetape
343
344     def create_sdprod(self, etape, nomsd):
345         """
346             Cette methode doit fabriquer le concept produit retourne
347             par l'etape etape et le nommer.
348
349             Elle est appelée à l'initiative de l'etape
350             pendant le processus de construction de cette etape :
351             methode __call__ de la classe CMD (OPER ou MACRO)
352
353             Ce travail est réalisé par le contexte supérieur
354             (etape.parent) car dans certains cas, le concept ne doit
355             pas etre fabriqué mais l'etape doit simplement utiliser
356             un concept préexistant.
357
358             Deux cas possibles :
359                     - Cas 1 : etape.reuse != None : le concept est réutilisé
360                     - Cas 2 : l'étape appartient à une macro qui a déclaré un
361                             concept de sortie qui doit etre produit par cette
362                             etape.
363             Dans le cas du JDC, le deuxième cas ne peut pas se produire.
364         """
365         sd = etape.get_sd_prod()
366         if sd != None and (etape.definition.reentrant == 'n' or etape.reuse is None):
367             # ATTENTION : On ne nomme la SD que dans le cas de non reutilisation
368             # d un concept. Commande non reentrante ou reuse absent.
369             self.NommerSdprod(sd, nomsd)
370         return sd
371
372     def NommerSdprod(self, sd, sdnom, restrict='non'):
373         """
374             Nomme la SD apres avoir verifie que le nommage est possible : nom
375             non utilise
376             Si le nom est deja utilise, leve une exception
377             Met le concept créé dans le concept global g_context
378         """
379         o = self.sds_dict.get(sdnom, None)
380         if isinstance(o, ASSD):
381             raise AsException(tr("Nom de concept deja defini : %s" % sdnom))
382         if sdnom in self._reserved_kw:
383             raise AsException(
384                tr( "Nom de concept invalide. '%s' est un mot-clé réservé." % sdnom))
385
386         # Ajoute a la creation (appel de reg_sd).
387         self.sds_dict[sdnom] = sd
388         sd.set_name(sdnom)
389
390         # En plus si restrict vaut 'non', on insere le concept dans le contexte
391         # du JDC
392         if restrict == 'non':
393             self.g_context[sdnom] = sd
394             # message.debug(SUPERV, "g_context[%r] = %s", sdnom, sd)
395
396     def reg_sd(self, sd):
397         """
398             Methode appelee dans l __init__ d un ASSD lors de sa creation
399             pour s enregistrer
400         """
401         return self.o_register(sd)
402
403     def delete_concept_after_etape(self, etape, sd):
404         """
405             Met à jour les étapes du JDC qui sont après etape suite à
406             la disparition du concept sd
407         """
408         # Cette methode est définie dans le noyau mais ne sert que pendant
409         # la phase de creation des etapes et des concepts. Il n'y a aucun
410         # traitement particulier à réaliser.
411         # Dans d'autres conditions, il faut surcharger cette méthode
412         return
413
414     def supprime(self):
415         N_OBJECT.OBJECT.supprime(self)
416         for etape in self.etapes:
417             etape.supprime()
418
419     def clean(self, netapes):
420         """Nettoie les `netapes` dernières étapes de la liste des étapes."""
421         if self.hist_etape:
422             return
423         for i in xrange(netapes):
424             e = self.etapes.pop()
425             jdc = e.jdc
426             parent = e.parent
427             e.supprime()
428             e.parent = parent
429             e.jdc = jdc
430             # message.debug(SUPERV, "JDC.clean - etape = %r - refcount(e) = %d",
431                           # e.nom, sys.getrefcount(e))
432             del self.index_etapes[e]
433
434     def get_file(self, unite=None, fic_origine='', fname=None):
435         """
436             Retourne le nom du fichier correspondant à un numero d'unité
437             logique (entier) ainsi que le source contenu dans le fichier
438         """
439         if self.appli:
440             # Si le JDC est relié à une application maitre, on délègue la
441             # recherche
442             return self.appli.get_file(unite, fic_origine)
443         else:
444             if unite != None:
445                 if os.path.exists("fort." + str(unite)):
446                     fname = "fort." + str(unite)
447             if fname == None:
448                 raise AsException(tr("Impossible de trouver le fichier correspondant"
449                                   " a l unite %s" % unite))
450             if not os.path.exists(fname):
451                 raise AsException (tr("%s n'est pas un fichier existant" % fname))
452             fproc = open(fname, 'r')
453             text = fproc.read()
454             fproc.close()
455             text = text.replace('\r\n', '\n')
456             linecache.cache[fname] = 0, 0, text.split('\n'), fname
457             return fname, text
458
459     def set_par_lot(self, par_lot, user_value=False):
460         """
461         Met le mode de traitement a PAR LOT
462         ou a COMMANDE par COMMANDE
463         en fonction de la valeur du mot cle PAR_LOT et
464         du contexte : application maitre ou pas
465
466         En PAR_LOT='NON', il n'y a pas d'ambiguité.
467         En PAR_LOT='OUI', E_SUPERV positionne l'attribut à 'NON' après la phase
468         d'analyse et juste avant la phase d'exécution.
469         `user_value` : permet de stocker la valeur choisie par l'utilisateur
470         pour l'interroger plus tard (par exemple dans `get_contexte_avant`).
471         """
472         # message.debug(SUPERV, "set par_lot = %r", par_lot)
473         if user_value:
474             self.par_lot_user = par_lot
475         if self.appli == None:
476             # Pas d application maitre
477             self.par_lot = par_lot
478         else:
479             # Avec application maitre
480             self.par_lot = 'OUI'
481
482     def accept(self, visitor):
483         """
484            Cette methode permet de parcourir l'arborescence des objets
485            en utilisant le pattern VISITEUR
486         """
487         visitor.visitJDC(self)
488
489     def interact(self):
490         """
491             Cette methode a pour fonction d'ouvrir un interpreteur
492             pour que l'utilisateur entre des commandes interactivement
493         """
494         CONTEXT.set_current_step(self)
495         try:
496             # Le module nommage utilise le module linecache pour accéder
497             # au source des commandes du jeu de commandes.
498             # Dans le cas d'un fichier, on accède au contenu de ce fichier
499             # Dans le cas de la console interactive, il faut pouvoir accéder
500             # aux commandes qui sont dans le buffer de la console
501             import linecache
502             import code
503             console = code.InteractiveConsole(
504                 self.g_context, filename="<console>")
505             linecache.cache["<console>"] = 0, 0, console.buffer, "<console>"
506             banner = """***********************************************
507 *          Interpreteur interactif %s
508 ***********************************************""" % self.code
509             console.interact(banner)
510         finally:
511             console = None
512             CONTEXT.unset_current_step()
513
514     def get_contexte_avant(self, etape):
515         """
516            Retourne le dictionnaire des concepts connus avant etape
517            On tient compte des commandes qui modifient le contexte
518            comme DETRUIRE ou les macros
519            Si etape == None, on retourne le contexte en fin de JDC
520         """
521         # L'étape courante pour laquelle le contexte a été calculé est
522         # mémorisée dans self.index_etape_courante
523         # XXX on pourrait faire mieux dans le cas PAR_LOT="NON" : en
524         # mémorisant l'étape
525         # courante pendant le processus de construction des étapes.
526         # Si on insère des commandes (par ex, dans EFICAS), il faut préalablement
527         # remettre ce pointeur à 0
528         # message.debug(SUPERV, "g_context : %s", [k for k, v in self.g_context.items() if isinstance(v, ASSD)])
529         # message.debug(SUPERV, "current_context : %s", [k for k, v in
530         # self.current_context.items() if isinstance(v, ASSD)])
531         if self.par_lot_user == 'NON':
532             d = self.current_context = self.g_context.copy()
533             if etape is None:
534                 return d
535             # retirer les sd produites par 'etape'
536             sd_names = [sd.nom for sd in etape.get_created_sd()]
537             # message.debug(SUPERV, "reuse : %s, sdprods : %s", etape.reuse,
538             # sd_names)
539             for nom in sd_names:
540                 try:
541                     del d[nom]
542                 except KeyError:
543                     from warnings import warn
544                     warn(
545                         tr("concept '%s' absent du contexte de %s" % (
546                             nom, self.nom)),
547                         RuntimeWarning, stacklevel=2)
548             return d
549         if etape:
550             index_etape = self.index_etapes[etape]
551         else:
552             index_etape = len(self.etapes)
553         if index_etape >= self.index_etape_courante:
554             # On calcule le contexte en partant du contexte existant
555             d = self.current_context
556             if self.index_etape_courante == 0 and self.context_ini:
557                 d.update(self.context_ini)
558             liste_etapes = self.etapes[self.index_etape_courante:index_etape]
559         else:
560             d = self.current_context = {}
561             if self.context_ini:
562                 d.update(self.context_ini)
563             liste_etapes = self.etapes
564
565         for e in liste_etapes:
566             if e is etape:
567                 break
568             if e.isactif():
569                 e.update_context(d)
570         self.index_etape_courante = index_etape
571         # message.debug(SUPERV, "returns : %s", [k for k, v in d.items() if
572         # isinstance(v, ASSD)])
573         return d
574
575     def get_global_contexte(self):
576         """Retourne "un" contexte global ;-)"""
577         # N'est utilisé que par INCLUDE (sauf erreur).
578         # g_context est remis à {} en PAR_LOT='OUI'. const_context permet
579         # de retrouver ce qui y a été mis par exec_compile.
580         # Les concepts n'y sont pas en PAR_LOT='OUI'. Ils sont ajoutés
581         # par get_global_contexte de la MACRO.
582         d = self.const_context.copy()
583         d.update(self.g_context)
584         return d
585
586     def get_contexte_courant(self, etape_courante=None):
587         """
588            Retourne le contexte tel qu'il est (ou 'sera' si on est en phase
589            de construction) au moment de l'exécution de l'étape courante.
590         """
591         if etape_courante is None:
592             etape_courante = CONTEXT.get_current_step()
593         return self.get_contexte_avant(etape_courante)
594
595     def get_concept(self, nomsd):
596         """
597             Méthode pour récuperer un concept à partir de son nom
598         """
599         co = self.get_contexte_courant().get(nomsd.strip(), None)
600         if not isinstance(co, ASSD):
601             co = None
602         return co
603
604     def get_concept_by_type(self, nomsd, typesd, etape):
605         """
606             Méthode pour récuperer un concept à partir de son nom et de son type.
607             Il aura comme père 'etape'.
608         """
609         assert issubclass(typesd, ASSD), typesd
610         co = typesd(etape=etape)
611         co.set_name(nomsd)
612         co.executed = 1
613         return co
614
615     def del_concept(self, nomsd):
616         """
617            Méthode pour supprimer la référence d'un concept dans le sds_dict.
618            Ne détruire pas le concept (différent de supprime).
619         """
620         try:
621             del self.sds_dict[nomsd.strip()]
622         except:
623             pass
624
625     def get_cmd(self, nomcmd):
626         """
627             Méthode pour recuperer la definition d'une commande
628             donnee par son nom dans les catalogues declares
629             au niveau du jdc
630         """
631         for cata in self.cata:
632             if hasattr(cata, nomcmd):
633                 return getattr(cata, nomcmd)
634
635     def append_reset(self, etape):
636         """
637            Ajoute une etape provenant d'un autre jdc a la liste des etapes
638            et remet à jour la parenté de l'étape et des concepts
639         """
640         self.etapes.append(etape)
641         self.index_etapes[etape] = len(self.etapes) - 1
642         etape.reparent(self)
643         etape.reset_jdc(self)
644
645     def sd_accessible(self):
646         """On peut acceder aux "valeurs" (jeveux) des ASSD si le JDC est en PAR_LOT="NON".
647         """
648         if CONTEXT.debug:
649             print ' `- JDC sd_accessible : PAR_LOT =', self.par_lot
650         return self.par_lot == 'NON'
651
652     def _build_reserved_kw_list(self):
653         """Construit la liste des mots-clés réservés (interdits pour le
654         nommage des concepts)."""
655         self._reserved_kw = set()
656         for cat in self.cata:
657             self._reserved_kw.update(
658                 [kw for kw in dir(cat) if len(kw) <= 8 and kw == kw.upper()])
659         self._reserved_kw.difference_update(
660             ['OPER', 'MACRO', 'BLOC', 'SIMP', 'FACT', 'FORM',
661              'GEOM', 'MCSIMP', 'MCFACT'])