Salome HOME
commentaire
[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 a interpreter un jeu de commandes
23 """
24
25 # Modules Python
26 from __future__ import absolute_import
27 from __future__ import print_function
28 try :
29    from builtins import str
30    from builtins import range
31 except : pass
32 import os
33 import traceback
34 import types
35 import sys
36 import linecache
37
38 # Modules EFICAS
39 from . import N_OBJECT
40 from . import N_CR
41 from .N_Exception import AsException, InterruptParsingError
42 from .N_ASSD import ASSD
43 from .strfunc import get_encoding
44 from six.moves import range
45
46
47 MemoryErrorMsg = """MemoryError :
48
49 En general, cette erreur se produit car la memoire utilisee hors du fortran
50 (jeveux) est importante.
51
52 Causes possibles :
53    - le calcul produit de gros objets Python dans une macro-commande ou
54      dans le jeu de commande lui-meme,
55    - le calcul appelle un solveur (MUMPS par exemple) ou un outil externe
56      qui a besoin de memoire hors jeveux,
57    - utilisation de jeveux dynamique,
58    - ...
59
60 Solution :
61    - distinguer la memoire limite du calcul (case "Memoire totale" de astk)
62      de la memoire reservee a jeveux (case "dont Aster"), le reste etant
63      disponible pour les allocations dynamiques.
64 """
65
66
67 class JDC(N_OBJECT.OBJECT):
68
69     """
70        Cette classe interprete un jeu de commandes fourni sous
71        la forme d'une chaine de caractères
72
73        Attributs de classe :
74
75        Attributs d'instance :
76
77     """
78     nature = "JDC"
79     CR = N_CR.CR
80     exec_init = """
81 import Accas
82 from Accas import _F
83 from Accas import *
84 NONE = None
85 """
86
87     from .N_utils import SEP
88
89     def __init__(self, definition=None, procedure=None, cata=None,
90                  cata_ord_dico=None, parent=None,
91                  nom='SansNom', appli=None, context_ini=None, **args):
92         self.procedure = procedure
93         self.definition = definition
94         self.cata = cata
95         if type(self.cata) != tuple and cata != None:
96             self.cata = (self.cata,)
97         self._build_reserved_kw_list()
98         self.cata_ordonne_dico = cata_ord_dico
99         self.nom = nom
100         self.appli = appli
101         self.parent = parent
102         self.context_ini = context_ini
103         # On conserve les arguments supplementaires. Il est possible de passer
104         # des informations globales au JDC par ce moyen. Il pourrait etre plus
105         # sur de mettre en place le mecanisme des mots-cles pour verifier la
106         # validite des valeurs passees.
107         # Ceci reste a faire
108         # On initialise avec les parametres de la definition puis on
109         # update avec ceux du JDC
110         self.args = self.definition.args
111         self.args.update(args)
112         self.nstep = 0
113         self.nsd = 0
114         self.par_lot = 'OUI'
115         self.par_lot_user = None
116         if definition:
117             self.regles = definition.regles
118             self.code = definition.code
119         else:
120             self.regles = ()
121             self.code = "CODE"
122         #
123         #  Creation de l objet compte rendu pour collecte des erreurs
124         #
125         self.cr = self.CR(debut="CR phase d'initialisation",
126                           fin="fin CR phase d'initialisation")
127         # on met le jdc lui-meme dans le context global pour l'avoir sous
128         # l'etiquette "jdc" dans le fichier de commandes
129         self.g_context = {'jdc': self}
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 etapes
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 consignees 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 as e:
161             if CONTEXT.debug:
162                 traceback.print_exc()
163             l = traceback.format_exception_only(SyntaxError, e)
164             self.cr.exception("Compilation impossible : " + ''.join(l))
165         except MemoryError as e:
166             self.cr.exception(MemoryErrorMsg)
167         except SystemError as e:
168             erreurs_connues = """
169 Causes possibles :
170  - offset too large : liste trop longue derrière un mot-cle.
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 methode execute le jeu de commandes compile 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 acceder
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 acceder
189         # aux commandes qui sont dans la chaine
190         import linecache
191         linecache.cache[self.nom] = 0, 0, self.procedure.split('\n'), self.nom
192         try:
193             exec(self.exec_init, self.g_context)
194             for obj_cata in self.cata:
195                 if type(obj_cata) == types.ModuleType:
196                     init2 = "from " + obj_cata.__name__ + " import *"
197                     exec(init2, self.g_context)
198
199             # Initialisation du contexte global pour l'evaluation des conditions de BLOC
200             # On utilise une copie de l'initialisation du contexte du jdc
201             self.condition_context = self.g_context.copy()
202
203             # Si l'attribut context_ini n'est pas vide, on ajoute au contexte global
204             # le contexte initial (--> permet d'evaluer un JDC en recuperant un contexte
205             # d'un autre par exemple)
206             if self.context_ini:
207                 self.g_context.update(self.context_ini)
208                 # Update du dictionnaire des concepts
209                 for sdnom, sd in list(self.context_ini.items()):
210                     if isinstance(sd, ASSD):
211                         self.sds_dict[sdnom] = sd
212
213             if self.appli != None:
214                 self.appli.affiche_infos(
215                     'Interpretation du fichier de commandes en cours ...')
216             # On sauve le contexte pour garder la memoire des constantes
217             # En mode edition (EFICAS) ou lors des verifications le contexte
218             # est recalcule
219             # mais les constantes sont perdues
220             self.const_context = self.g_context
221             exec(self.proc_compile, self.g_context)
222
223             CONTEXT.unset_current_step()
224             if self.appli != None:
225                 self.appli.affiche_infos('')
226
227         except InterruptParsingError:
228             # interrupt the command file parsing used by FIN to ignore the end
229             # of the file
230             pass
231
232         except EOFError:
233             # Exception utilise pour interrompre un jeu
234             # de commandes avant la fin
235             # Fonctionnement normal, ne doit pas etre considere comme une
236             # erreur
237             CONTEXT.unset_current_step()
238             self.affiche_fin_exec()
239             self.traiter_fin_exec('commande')
240
241         except AsException as e:
242             # une erreur a ete identifiee
243             if CONTEXT.debug:
244                 traceback.print_exc()
245             # l'exception a ete recuperee avant (ou, comment ?),
246             # donc on cherche dans le texte
247             txt = str(e)
248             if txt.find('MemoryError') >= 0:
249                 txt = MemoryErrorMsg
250             self.cr.exception(txt)
251             CONTEXT.unset_current_step()
252
253         except NameError as e:
254             etype, value, tb = sys.exc_info()
255             l = traceback.extract_tb(tb)
256             s = traceback.format_exception_only("Erreur de nom", e)[0][:-1]
257             msg = "erreur de syntaxe,  %s ligne %d" % (s, l[-1][1])
258             if CONTEXT.debug:
259                 traceback.print_exc()
260             self.cr.exception(msg)
261             CONTEXT.unset_current_step()
262
263        # except self.UserError as exc_val:
264        #     self.traiter_user_exception(exc_val)
265        #     CONTEXT.unset_current_step()
266        #     self.affiche_fin_exec()
267        #     self.traiter_fin_exec('commande')
268
269         except:
270             # erreur inattendue
271             # sys_exc_typ,sys_exc_value,sys_exc_frame = sys_exc.info()
272             # (tuple de 3 elements)
273             if CONTEXT.debug:
274                 traceback.print_exc()
275
276             traceback.print_exc()
277
278             exc_typ, exc_val, exc_fr = sys.exc_info()
279             l = traceback.format_exception(exc_typ, exc_val, exc_fr)
280             self.cr.exception(
281                 "erreur non prevue et non traitee prevenir la maintenance " + '\n' + ''.join(l))
282             del exc_typ, exc_val, exc_fr
283             CONTEXT.unset_current_step()
284
285     def affiche_fin_exec(self):
286         """
287            Cette methode realise l'affichage final des statistiques de temps
288            apres l'execution de toutes
289            les commandes en mode commande par commande ou par lot
290            Elle doit etre surchargee pour en introduire un
291         """
292         return
293
294     def traiter_fin_exec(self, mode, etape=None):
295         """
296            Cette methode realise un traitement final apres l'execution de toutes
297            les commandes en mode commande par commande ou par lot
298            Par defaut il n'y a pas de traitement. Elle doit etre surchargee
299            pour en introduire un
300         """
301         print ( "FIN D'EXECUTION %s %s" %s( mode, etape))
302
303     def traiter_user_exception(self, exc_val):
304         """Cette methode realise un traitement sur les exceptions utilisateur
305            Par defaut il n'y a pas de traitement. La methode doit etre
306            surchargee pour en introduire un.
307         """
308         return
309
310     def register(self, etape):
311         """
312            Cette methode ajoute etape dans la liste des etapes : self.etapes
313            et retourne un numero d'enregistrement
314         """
315         self.etapes.append(etape)
316         self.index_etapes[etape] = len(self.etapes) - 1
317         return self.g_register(etape)
318
319     def o_register(self, sd):
320         """
321            Retourne un identificateur pour concept
322         """
323         self.nsd = self.nsd + 1
324         nom = sd.idracine + self.SEP + repr(self.nsd)
325         return nom
326
327     def g_register(self, etape):
328         """
329             Retourne un identificateur pour etape
330         """
331         self.nstep = self.nstep + 1
332         idetape = etape.idracine + self.SEP + repr(self.nstep)
333         return idetape
334
335     def create_sdprod(self, etape, nomsd):
336         """
337             Cette methode doit fabriquer le concept produit retourne
338             par l'etape etape et le nommer.
339
340             Elle est appelee a l'initiative de l'etape
341             pendant le processus de construction de cette etape :
342             methode __call__ de la classe CMD (OPER ou MACRO)
343
344             Ce travail est realise par le contexte superieur
345             (etape.parent) car dans certains cas, le concept ne doit
346             pas etre fabrique mais l'etape doit simplement utiliser
347             un concept preexistant.
348
349             Deux cas possibles :
350                     - Cas 1 : etape.reuse != None : le concept est reutilise
351                     - Cas 2 : l'etape appartient a une macro qui a declare un
352                             concept de sortie qui doit etre produit par cette
353                             etape.
354             Dans le cas du JDC, le deuxième cas ne peut pas se produire.
355         """
356         sd = etape.get_sd_prod()
357         if sd != None and (etape.definition.reentrant == 'n' or etape.reuse is None):
358             # ATTENTION : On ne nomme la SD que dans le cas de non reutilisation
359             # d un concept. Commande non reentrante ou reuse absent.
360             self.NommerSdprod(sd, nomsd)
361         return sd
362
363     def NommerSdprod(self, sd, sdnom, restrict='non'):
364         """
365             Nomme la SD apres avoir verifie que le nommage est possible : nom
366             non utilise
367             Si le nom est deja utilise, leve une exception
368             Met le concept cree dans le concept global g_context
369         """
370         o = self.sds_dict.get(sdnom, None)
371         if isinstance(o, ASSD):
372             raise AsException("Nom de concept deja defini : %s" % sdnom)
373         if sdnom in self._reserved_kw:
374             raise AsException(
375                 "Nom de concept invalide. '%s' est un mot-cle reserve." % sdnom)
376
377         # Ajoute a la creation (appel de reg_sd).
378         self.sds_dict[sdnom] = sd
379         sd.set_name(sdnom)
380
381         # En plus si restrict vaut 'non', on insere le concept dans le contexte
382         # du JDC
383         if restrict == 'non':
384             self.g_context[sdnom] = sd
385
386     def reg_sd(self, sd):
387         """
388             Methode appelee dans l __init__ d un ASSD lors de sa creation
389             pour s enregistrer
390         """
391         return self.o_register(sd)
392
393     def delete_concept_after_etape(self, etape, sd):
394         """
395             Met a jour les etapes du JDC qui sont après etape suite a
396             la disparition du concept sd
397         """
398         # Cette methode est definie dans le noyau mais ne sert que pendant
399         # la phase de creation des etapes et des concepts. Il n'y a aucun
400         # traitement particulier a realiser.
401         # Dans d'autres conditions, il faut surcharger cette methode
402         return
403
404     def supprime(self):
405         N_OBJECT.OBJECT.supprime(self)
406         for etape in self.etapes:
407             etape.supprime()
408
409     def clean(self, netapes):
410         """Nettoie les `netapes` dernières etapes de la liste des etapes."""
411         if self.hist_etape:
412             return
413         for i in range(netapes):
414             e = self.etapes.pop()
415             jdc = e.jdc
416             parent = e.parent
417             e.supprime()
418             e.parent = parent
419             e.jdc = jdc
420             del self.index_etapes[e]
421
422     def get_file(self, unite=None, fic_origine='', fname=None):
423         """
424             Retourne le nom du fichier correspondant a un numero d'unite
425             logique (entier) ainsi que le source contenu dans le fichier
426         """
427         if self.appli:
428             # Si le JDC est relie a une application maitre, on delègue la
429             # recherche
430             return self.appli.get_file(unite, fic_origine)
431         else:
432             if unite != None:
433                 if os.path.exists("fort." + str(unite)):
434                     fname = "fort." + str(unite)
435             if fname == None:
436                 raise AsException("Impossible de trouver le fichier correspondant"
437                                   " a l unite %s" % unite)
438             if not os.path.exists(fname):
439                 raise AsException("%s n'est pas un fichier existant" % fname)
440             fproc = open(fname, 'r')
441             text = fproc.read()
442             fproc.close()
443             text = text.replace('\r\n', '\n')
444             linecache.cache[fname] = 0, 0, text.split('\n'), fname
445             return fname, text
446
447     def set_par_lot(self, par_lot, user_value=False):
448         """
449         Met le mode de traitement a PAR LOT
450         ou a COMMANDE par COMMANDE
451         en fonction de la valeur du mot cle PAR_LOT et
452         du contexte : application maitre ou pas
453
454         En PAR_LOT='NON', il n'y a pas d'ambiguite.
455         d'analyse et juste avant la phase d'execution.
456         `user_value` : permet de stocker la valeur choisie par l'utilisateur
457         pour l'interroger plus tard (par exemple dans `get_contexte_avant`).
458         """
459         if user_value:
460             self.par_lot_user = par_lot
461         if self.appli == None:
462             # Pas d application maitre
463             self.par_lot = par_lot
464         else:
465             # Avec application maitre
466             self.par_lot = 'OUI'
467
468     def accept(self, visitor):
469         """
470            Cette methode permet de parcourir l'arborescence des objets
471            en utilisant le pattern VISITEUR
472         """
473         visitor.visitJDC(self)
474
475     def interact(self):
476         """
477             Cette methode a pour fonction d'ouvrir un interpreteur
478             pour que l'utilisateur entre des commandes interactivement
479         """
480         CONTEXT.set_current_step(self)
481         try:
482             # Le module nommage utilise le module linecache pour acceder
483             # au source des commandes du jeu de commandes.
484             # Dans le cas d'un fichier, on accède au contenu de ce fichier
485             # Dans le cas de la console interactive, il faut pouvoir acceder
486             # aux commandes qui sont dans le buffer de la console
487             import linecache
488             import code
489             console = code.InteractiveConsole(
490                 self.g_context, filename="<console>")
491             linecache.cache["<console>"] = 0, 0, console.buffer, "<console>"
492             banner = """***********************************************
493 *          Interpreteur interactif %s
494 ***********************************************""" % self.code
495             console.interact(banner)
496         finally:
497             console = None
498             CONTEXT.unset_current_step()
499
500     def get_contexte_avant(self, etape):
501         """
502            Retourne le dictionnaire des concepts connus avant etape
503            On tient compte des commandes qui modifient le contexte
504            comme DETRUIRE ou les macros
505            Si etape == None, on retourne le contexte en fin de JDC
506         """
507         # L'etape courante pour laquelle le contexte a ete calcule est
508         # memorisee dans self.index_etape_courante
509         # XXX on pourrait faire mieux dans le cas PAR_LOT="NON" : en
510         # memorisant l'etape
511         # courante pendant le processus de construction des etapes.
512         # Si on insère des commandes (par ex, dans EFICAS), il faut prealablement
513         # remettre ce pointeur a 0
514         # self.current_context.items() if isinstance(v, ASSD)])
515         if self.par_lot_user == 'NON':
516             d = self.current_context = self.g_context.copy()
517             if etape is None:
518                 return d
519             # retirer les sd produites par 'etape'
520             sd_names = [sd.nom for sd in etape.get_created_sd()]
521             for nom in sd_names:
522                 try:
523                     del d[nom]
524                 except KeyError:
525                     from warnings import warn
526                     warn(
527                         "concept '%s' absent du contexte de %s" % (
528                             nom, self.nom),
529                         RuntimeWarning, stacklevel=2)
530             return d
531         if etape:
532             index_etape = self.index_etapes[etape]
533         else:
534             index_etape = len(self.etapes)
535         if index_etape >= self.index_etape_courante:
536             # On calcule le contexte en partant du contexte existant
537             d = self.current_context
538             if self.index_etape_courante == 0 and self.context_ini:
539                 d.update(self.context_ini)
540             liste_etapes = self.etapes[self.index_etape_courante:index_etape]
541         else:
542             d = self.current_context = {}
543             if self.context_ini:
544                 d.update(self.context_ini)
545             liste_etapes = self.etapes
546
547         for e in liste_etapes:
548             if e is etape:
549                 break
550             if e.isactif():
551                 e.update_context(d)
552         self.index_etape_courante = index_etape
553         return d
554
555     def get_global_contexte(self):
556         """Retourne "un" contexte global ;-)"""
557         # N'est utilise que par INCLUDE (sauf erreur).
558         # g_context est remis a {} en PAR_LOT='OUI'. const_context permet
559         # de retrouver ce qui y a ete mis par exec_compile.
560         # Les concepts n'y sont pas en PAR_LOT='OUI'. Ils sont ajoutes
561         # par get_global_contexte de la MACRO.
562         d = self.const_context.copy()
563         d.update(self.g_context)
564         return d
565
566     def get_contexte_courant(self, etape_courante=None):
567         """
568            Retourne le contexte tel qu'il est (ou 'sera' si on est en phase
569            de construction) au moment de l'execution de l'etape courante.
570         """
571         if etape_courante is None:
572             etape_courante = CONTEXT.get_current_step()
573         return self.get_contexte_avant(etape_courante)
574
575     def get_concept(self, nomsd):
576         """
577             Methode pour recuperer un concept a partir de son nom
578         """
579         co = self.get_contexte_courant().get(nomsd.strip(), None)
580         if not isinstance(co, ASSD):
581             co = None
582         return co
583
584     def get_concept_by_type(self, nomsd, typesd, etape):
585         """
586             Methode pour recuperer un concept a partir de son nom et de son type.
587             Il aura comme père 'etape'.
588         """
589         assert issubclass(typesd, ASSD), typesd
590         co = typesd(etape=etape)
591         co.set_name(nomsd)
592         co.executed = 1
593         return co
594
595     def del_concept(self, nomsd):
596         """
597            Methode pour supprimer la reference d'un concept dans le sds_dict.
598            Ne detruire pas le concept (different de supprime).
599         """
600         try:
601             del self.sds_dict[nomsd.strip()]
602         except:
603             pass
604
605     def get_cmd(self, nomcmd):
606         """
607             Methode pour recuperer la definition d'une commande
608             donnee par son nom dans les catalogues declares
609             au niveau du jdc
610         """
611         for cata in self.cata:
612             if hasattr(cata, nomcmd):
613                 return getattr(cata, nomcmd)
614
615     def append_reset(self, etape):
616         """
617            Ajoute une etape provenant d'un autre jdc a la liste des etapes
618            et remet a jour la parente de l'etape et des concepts
619         """
620         self.etapes.append(etape)
621         self.index_etapes[etape] = len(self.etapes) - 1
622         etape.reparent(self)
623         etape.reset_jdc(self)
624
625     def sd_accessible(self):
626         """On peut acceder aux "valeurs" (jeveux) des ASSD si le JDC est en PAR_LOT="NON".
627         """
628         if CONTEXT.debug:
629             print((' `- JDC sd_accessible : PAR_LOT =', self.par_lot))
630         return self.par_lot == 'NON'
631
632     def _build_reserved_kw_list(self):
633         """Construit la liste des mots-cles reserves (interdits pour le
634         nommage des concepts)."""
635         self._reserved_kw = set()
636         for cat in self.cata:
637             self._reserved_kw.update(
638                 [kw for kw in dir(cat) if len(kw) <= 8 and kw == kw.upper()])
639         self._reserved_kw.difference_update(
640             ['OPER', 'MACRO', 'BLOC', 'SIMP', 'FACT', 'FORM',
641              'GEOM', 'MCSIMP', 'MCFACT'])