]> SALOME platform Git repositories - modules/yacs.git/commitdiff
Salome HOME
Yacs decorator
authorOvidiu Mircescu <ovidiu.mircescu@edf.fr>
Tue, 1 Sep 2020 13:33:21 +0000 (15:33 +0200)
committerOvidiu Mircescu <ovidiu.mircescu@edf.fr>
Tue, 1 Sep 2020 13:33:21 +0000 (15:33 +0200)
Build a yacs schema out of a decorated python script.

src/py2yacs/CMakeLists.txt
src/py2yacs/Test/CMakeLists.txt
src/py2yacs/Test/CTestTestfileInstall.cmake
src/py2yacs/Test/formule.py [new file with mode: 0755]
src/py2yacs/Test/testDeco.py [new file with mode: 0755]
src/py2yacs/Test/testforeach.py [new file with mode: 0755]
src/py2yacs/py2yacs.py
src/py2yacs/yacsbuild.py [new file with mode: 0755]
src/py2yacs/yacsdecorator.py [new file with mode: 0644]
src/py2yacs/yacstools.py [new file with mode: 0644]

index c66943ca48110bd53b80d7c2cb235f09d2d2f18f..487c7a441933c791bdbe3125329098aab80ecaed 100644 (file)
@@ -57,6 +57,13 @@ INSTALL(FILES ${_py2yacs_headers} DESTINATION ${SALOME_INSTALL_HEADERS})
 
 SET( _py_SCRIPTS
   py2yacs.py
+  yacsdecorator.py
+  yacstools.py
+  )
+
+SET(_py_PROGS
+  yacsbuild.py
   )
 
 SALOME_INSTALL_SCRIPTS("${_py_SCRIPTS}"  ${SALOME_INSTALL_PYTHON})
+SALOME_INSTALL_SCRIPTS("${_py_PROGS}" ${SALOME_INSTALL_SCRIPT_SCRIPTS})
index 56cc8c068e7b6ea302f8cc9cc605d2149c158b23..1ee8cc9bf2411eb7b2d632f53b94cd92a74d28de 100644 (file)
@@ -42,7 +42,7 @@ TARGET_LINK_LIBRARIES(test_py2yacs py2yacslib)
 # For salome test
 IF(NOT WIN32)
   # This test needs a running salome session
-  # TODO: Run the test in a salome session
+  # TODO: Run the test in a salome session with make test
   #ADD_TEST(TestPy2yacs TestPy2yacs)
   #SET_TESTS_PROPERTIES(TestPy2yacs PROPERTIES
   #                     ENVIRONMENT "PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_CURRENT_SOURCE_DIR}/..:$ENV{PYTHONPATH}"
@@ -52,13 +52,20 @@ IF(NOT WIN32)
   SET(LOCAL_TEST_FILES
     bad_parsers.py
     err_py2yacs_invalid.py
+    testforeach.py
+    formule.py
+    )
+  SET(LOCAL_TEST_SCRIPTS
+    testDeco.py
     )
   INSTALL(FILES ${LOCAL_TEST_FILES}
           DESTINATION ${LOCAL_TEST_DIR})
+  INSTALL(PROGRAMS ${LOCAL_TEST_SCRIPTS}
+        DESTINATION ${LOCAL_TEST_DIR})
   INSTALL(FILES CTestTestfileInstall.cmake
           DESTINATION ${LOCAL_TEST_DIR}
           RENAME CTestTestfile.cmake)
   INSTALL(TARGETS TestPy2yacs test_py2yacs
           DESTINATION ${LOCAL_TEST_DIR})
 
-ENDIF()
\ No newline at end of file
+ENDIF()
index 9187bfae3728beded3b0cb88f281670933019161..6ccef91de3b5c0224bcc37efd436ba7533153d96 100644 (file)
@@ -25,4 +25,9 @@ IF(NOT WIN32)
                                     LABELS "${COMPONENT_NAME}"
                       )
 
+  SET(TEST_NAME ${COMPONENT_NAME}_PyDecorator)
+  ADD_TEST(${TEST_NAME} ${SALOME_TEST_DRIVER} ${TIMEOUT} testDeco.py)
+  SET_TESTS_PROPERTIES(${TEST_NAME} PROPERTIES
+                                    LABELS "${COMPONENT_NAME}"
+                      )
 ENDIF()
diff --git a/src/py2yacs/Test/formule.py b/src/py2yacs/Test/formule.py
new file mode 100755 (executable)
index 0000000..eba73c6
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+import yacsdecorator
+
+@yacsdecorator.leaf
+def f1(x,y):
+  r = x+y
+  return r
+
+@yacsdecorator.leaf
+def f2(a):
+  r = a + 2
+  return r
+
+@yacsdecorator.leaf
+def f3(x, y):
+  s = x+y
+  p = x*y
+  return s,p
+
+@yacsdecorator.bloc
+def b1():
+  x = f1(x=3,y=4)
+  a,b = f3(x, 2)
+  f2(x)
+  r = f1(a,b)
+  return r
+
+if __name__ == '__main__':
+  r = b1()
+  print("result:", r)
diff --git a/src/py2yacs/Test/testDeco.py b/src/py2yacs/Test/testDeco.py
new file mode 100755 (executable)
index 0000000..dc68a93
--- /dev/null
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+# Copyright (C) 2006-2020  CEA/DEN, EDF R&D
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+
+import unittest
+import tempfile
+import os
+import subprocess
+
+import SALOMERuntime
+import loader
+import pilot
+
+dir_test = tempfile.mkdtemp(suffix=".yacstest")
+
+class TestDeco(unittest.TestCase):
+
+    def setUp(self):
+      SALOMERuntime.RuntimeSALOME_setRuntime()
+
+    def test_t1(self):
+      """
+      Schema:
+      jdd -> foreach -> post
+           > f2
+         /
+      f1 -> f3 -> f1
+      """
+      import testforeach
+      expected_1, expected_2 = testforeach.main()
+      yacs_schema_file = os.path.join(dir_test, "schema_t1.xml")
+      yacs_build_command = "yacsbuild.py"
+      test_script = "testforeach.py"
+      main_function_name = "main"
+      subprocess.run([yacs_build_command,
+                      test_script, main_function_name, yacs_schema_file])
+      l = loader.YACSLoader()
+      ex = pilot.ExecutorSwig()
+      proc = l.load(yacs_schema_file)
+      ex.RunW(proc,0)
+      obtained_1 = proc.getChildByName("post_0").getOutputPort("s").getPyObj()
+      obtained_2 = proc.getChildByName("f1_1").getOutputPort("r").getPyObj()
+      self.assertEqual(expected_1, obtained_1)
+      self.assertEqual(expected_2, obtained_2)
+
+    def test_t2(self):
+      """
+      Foreach initialized by value.
+      """
+      import testforeach
+      expected_1, expected_2 = testforeach.mainbloc()
+      yacs_schema_file = os.path.join(dir_test, "schema_t2.xml")
+      yacs_build_command = "yacsbuild.py"
+      test_script = "testforeach.py"
+      main_function_name = "mainbloc"
+      subprocess.run([yacs_build_command,
+                      test_script, main_function_name, yacs_schema_file])
+      l = loader.YACSLoader()
+      ex = pilot.ExecutorSwig()
+      proc = l.load(yacs_schema_file)
+      ex.RunW(proc,0)
+      obtained_1 = proc.getChildByName("output_fr_0").getOutputPort("s_0").getPyObj()
+      obtained_2 = proc.getChildByName("output_fr_0").getOutputPort("p_1").getPyObj()
+      self.assertEqual(expected_1, obtained_1)
+      self.assertEqual(expected_2, obtained_2)
+
+    def test_t3(self):
+      """
+      Foreach on 2 levels.
+      """
+      import testforeach
+      expected = testforeach.maindoublefr()
+      yacs_schema_file = os.path.join(dir_test, "schema_t3.xml")
+      yacs_build_command = "yacsbuild.py"
+      test_script = "testforeach.py"
+      main_function_name = "maindoublefr"
+      subprocess.run([yacs_build_command,
+                      test_script, main_function_name, yacs_schema_file])
+      l = loader.YACSLoader()
+      ex = pilot.ExecutorSwig()
+      proc = l.load(yacs_schema_file)
+      ex.RunW(proc,0)
+      obtained = proc.getChildByName("output_doublefr_0").getOutputPort("r_0_0").getPyObj()
+      self.assertEqual(expected, obtained)
+
+if __name__ == '__main__':
+  file_test = os.path.join(dir_test,"UnitTestsResult")
+  with open(file_test, 'a') as f:
+      f.write("  --- TEST src/py2yacs: testDeco.py\n")
+      suite = unittest.makeSuite(TestDeco)
+      result=unittest.TextTestRunner(f, descriptions=1, verbosity=1).run(suite)
+  sys.exit(not result.wasSuccessful())
diff --git a/src/py2yacs/Test/testforeach.py b/src/py2yacs/Test/testforeach.py
new file mode 100755 (executable)
index 0000000..5df0dfe
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+import yacsdecorator
+import formule
+
+@yacsdecorator.leaf
+def f1(x,y):
+  r = x+y
+  return r
+
+@yacsdecorator.leaf
+def f2(a):
+  r = a + 2
+  return r
+
+@yacsdecorator.leaf
+def f3(x, y):
+  s = x+y
+  p = x*y
+  return s,p
+
+@yacsdecorator.leaf
+def jdd():
+  r = list(range(10))
+  return r
+
+@yacsdecorator.foreach
+def fr(v):
+  a,b = f3(v, 2)
+  return a,b
+
+@yacsdecorator.foreach
+def fr2(v):
+  r = f2(v)
+  return r
+
+@yacsdecorator.foreach
+def doublefr(v):
+  return fr2(v)
+
+@yacsdecorator.leaf
+def post(t):
+  s = 0
+  for e in t:
+    s += e
+  return s
+
+@yacsdecorator.bloc
+def mainbloc():
+  return fr(range(10))
+
+@yacsdecorator.bloc
+def maindoublefr():
+  vals = [ list(range(x)) for x in range(10)]
+  return doublefr(vals)
+
+@yacsdecorator.bloc
+def main():
+  vals = jdd()
+  result = fr2(vals)
+  r1 = post(result)
+  x = formule.f1(x=3,y=4)
+  a,b = formule.f3(x, 2)
+  formule.f2(x)
+  r2 = formule.f1(a,b)
+  return r1,r2
+
+if __name__ == '__main__':
+  v1, v2 = main()
+  print("v1:", v1)
+  print("v2:", v2)
index 364bd8d6adb28fc5982e54469b4434115b06f009..3ee6251aed7f43e596c63339525e7d00f837ee50 100644 (file)
@@ -36,7 +36,7 @@ class FunctionProperties:
     result+= "  Imports:"+ str(self.imports) + "\n"
     return result
 
-class v(ast.NodeVisitor):
+class VisitAST(ast.NodeVisitor):
   def visit_Module(self, node):
     accepted_tokens = ["Import", "ImportFrom", "FunctionDef", "ClassDef"]
     self.global_errors=[]
@@ -140,10 +140,35 @@ def get_properties(text_file):
   except SyntaxError as err:
     import traceback
     return [], ["".join(traceback.format_exception_only(SyntaxError,err))]
-  w=v()
+  w=VisitAST()
   w.visit(bt)
   return w.functions, w.global_errors
 
+def function_properties(python_path, fn_name):
+  """
+  python_path : path to a python file
+  fn_name : name of a function in the file
+  return : properties of the function. see class FunctionProperties
+  """
+  with open(python_path, 'r') as f:
+    text_file = f.read()
+  functions,errors = get_properties(text_file)
+  result = [fn for fn in functions if fn.name == fn_name]
+  if len(result) < 1:
+    raise Exception("Function not found: {}".format(fn_name))
+  result = result[0]
+  error_string = ""
+  #if len(errors) > 0:
+    #error_string += "Global errors in file {}\n".format(python_path)
+    #error_string += '\n'.join(errors)
+    #raise Exception(error_string)
+  if len(result.errors) > 0:
+    error_string += "Errors when parsing function {}\n".format(fn_name)
+    error_string += '\n'.join(result.errors)
+    raise Exception(error_string)
+  return result
+
+
 def main(python_path, yacs_path, function_name="_exec"):
   with open(python_path, 'r') as f:
     text_file = f.read()
diff --git a/src/py2yacs/yacsbuild.py b/src/py2yacs/yacsbuild.py
new file mode 100755 (executable)
index 0000000..61aaaf6
--- /dev/null
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+# Copyright (C) 2006-2020  CEA/DEN, EDF R&D
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+
+if __name__ == '__main__':
+  import argparse
+  parser = argparse.ArgumentParser(
+    description="Build a YACS schema out of a decorated python script.")
+  parser.add_argument("path", help='Path to the script.')
+  parser.add_argument("mainbloc",
+        help='Name of the function containing the main bloc of the schema.')
+  parser.add_argument("yacsfile", help='Path to the output yacs file.')
+  args = parser.parse_args()
+  import yacsdecorator
+  yacsdecorator.activateYacsMode()
+  import yacstools
+  fn = yacstools.getFunction(args.path, args.mainbloc)
+  fn()
+  yacsdecorator.finalize(args.yacsfile)
diff --git a/src/py2yacs/yacsdecorator.py b/src/py2yacs/yacsdecorator.py
new file mode 100644 (file)
index 0000000..462981e
--- /dev/null
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# Copyright (C) 2006-2020  CEA/DEN, EDF R&D
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+import sys
+
+# this is a pointer to the module object instance itself.
+this_module = sys.modules[__name__]
+
+class OutputPort:
+  def __init__(self, yacs_node, yacs_port):
+    self.yacs_node = yacs_node
+    self.yacs_port = yacs_port
+
+class LeafNodeType:
+  def __init__(self, path, fn_name, inputs, outputs):
+    self.path = path
+    self.fn_name = fn_name
+    self.inputs = inputs
+    self.outputs = outputs
+    self.number = 0
+
+  def newName(self):
+    name = self.fn_name + "_" + str(self.number)
+    self.number += 1
+    return name
+
+  def createNewNode(self, inputs):
+    """
+    inputs : dict {input_name:value}
+    """
+    generator = getGenerator()
+    output_ports = generator.createScriptNode(self, inputs)
+    return output_ports
+
+def leaf(f):
+  """
+  Decorator for python scripts.
+  """
+  if this_module._exec_mode == this_module._default_mode:
+    return f
+  co = f.__code__
+  import py2yacs
+  props = py2yacs.function_properties(co.co_filename, co.co_name)
+  nodeType = LeafNodeType(co.co_filename, co.co_name,
+                          props.inputs, props.outputs)
+  def my_func(*args, **kwargs):
+    if len(args) + len(kwargs) != len(nodeType.inputs):
+      mes = "Wrong number of arguments when calling function '{}'.\n".format(
+                                                               nodeType.fn_name)
+      mes += " {} arguments expected and {} arguments found.\n".format(
+                                  len(nodeType.inputs), len(args) + len(kwargs))
+      raise Exception(mes)
+    idx = 0
+    args_dic = {}
+    for a in args:
+      args_dic[nodeType.inputs[idx]] = a
+      idx += 1
+    for k,v in kwargs.items():
+      args_dic[k] = v
+    if len(args_dic) != len(nodeType.inputs):
+      mes="Wrong arguments when calling function {}.\n".format(nodeType.fn_name)
+      raise Exception(mes)
+    return nodeType.createNewNode(args_dic)
+  return my_func
+
+def bloc(f):
+  """
+  Decorator for blocs.
+  """
+  #co = f.__code__
+  #print("bloc :", co.co_name)
+  #print("  file:", co.co_filename)
+  #print("  line:", co.co_firstlineno)
+  #print("  args:", co.co_varnames)
+  return f
+
+def foreach(f):
+  """
+  Decorator to generate foreach blocs
+  """
+  if this_module._exec_mode == this_module._default_mode:
+    return default_foreach(f)
+  elif this_module._exec_mode == this_module._yacs_mode:
+    return yacs_foreach(f)
+
+def default_foreach(f):
+  def my_func(lst):
+    result = []
+    for e in lst:
+      result.append(f(e))
+    t_result = result
+    if len(result) > 0 :
+      if type(result[0]) is tuple:
+        # transform the list of tuples in a tuple of lists
+        l_result = []
+        for e in result[0]:
+          l_result.append([])
+        for t in result:
+          idx = 0
+          for e in t:
+            l_result[idx].append(e)
+            idx += 1
+        t_result = tuple(l_result)
+    return t_result
+  return my_func
+
+def yacs_foreach(f):
+  #co = f.__code__
+  #import yacsvisit
+  #props = yacsvisit.main(co.co_filename, co.co_name)
+  def my_func(input_list):
+    fn_name = f.__code__.co_name
+    generator = getGenerator()
+    sample_port = generator.beginForeach(fn_name, input_list)
+    output_list = f(sample_port)
+    output_list = generator.endForeach(output_list)
+    return output_list
+  return my_func
+
+class SchemaGenerator():
+  """
+  Link to Salome for YACS schema generation.
+  """
+  def __init__(self):
+    import SALOMERuntime
+    SALOMERuntime.RuntimeSALOME.setRuntime()
+    self.runtime = SALOMERuntime.getSALOMERuntime()
+    self.proc = self.runtime.createProc("GeneratedSchema")
+    self.proc.setProperty("executor","workloadmanager")
+    self.containers = {}
+    self.pyobjtype = self.runtime.getTypeCode("pyobj")
+    self.seqpyobjtype = self.runtime.getTypeCode("seqpyobj")
+    self.bloc_stack = [self.proc]
+    self.name_index = 0 # used to ensure unique names
+
+  def newName(self, name):
+    new_name = name + "_" + str(self.name_index)
+    self.name_index += 1
+    return new_name
+
+  def getContextName(self):
+    context_name = ""
+    if len(self.bloc_stack) > 1:
+      # We are in a block
+      block_path = ".".join([ b.getName() for b in self.bloc_stack[1:] ])
+      context_name = block_path + "."
+    return context_name
+
+  def getContainer(self, container_type):
+    """
+    A new container may be created if it does not already exist for this type.
+    """
+    if container_type not in self.containers:
+      cont=self.proc.createContainer(container_type,"Salome")
+      #cont.setProperty("nb_proc_per_node","0")
+      cont.setProperty("type","multi")
+      cont.usePythonCache(False)
+      cont.attachOnCloning()
+      self.containers[container_type] = cont
+    return self.containers[container_type]
+
+  def createScript(self, file_path, function_name, inputs, outputs):
+    import inspect
+    stack = inspect.stack()
+    stack_info = "Call stack\n"
+    # skip the first 4 levels in the stack
+    for level in stack[4:-1] :
+      info = inspect.getframeinfo(level[0])
+      stack_info += "file: {}, line: {}, function: {}, context: {}\n".format(
+        info.filename, info.lineno, info.function, info.code_context)
+     
+    if len(outputs) == 0:
+      result = ""
+    elif len(outputs) == 1:
+      result = "{} = ".format(outputs[0])
+    else:
+      result = ",".join(outputs)
+      result += " = "
+
+    if len(inputs) == 0:
+      params = ""
+    elif len(inputs) == 1:
+      params = "{} ".format(inputs[0])
+    else:
+      params = ",".join(inputs)
+    
+    script = """'''
+{call_stack}
+'''
+import yacstools
+study_function = yacstools.getFunction("{file_path}", "{function_name}")
+{result}study_function({parameters})
+""".format(call_stack=stack_info,
+           file_path=file_path,
+           function_name=function_name,
+           result=result,
+           parameters=params)
+    return script
+
+  def createScriptNode(self, leaf, input_values):
+    node_name = leaf.newName()
+    file_path = leaf.path
+    function_name = leaf.fn_name
+    inputs = leaf.inputs # names
+    outputs = leaf.outputs # names
+    script = self.createScript(file_path, function_name, inputs, outputs)
+    container = self.getContainer("generic_cont")
+    new_node = self.runtime.createScriptNode("Salome", node_name)
+    new_node.setContainer(container)
+    new_node.setExecutionMode("remote")
+    new_node.setScript(script)
+    self.bloc_stack[-1].edAddChild(new_node)
+    # create ports
+    for p in inputs:
+      new_node.edAddInputPort(p, self.pyobjtype)
+    output_obj_list = []
+    for p in outputs:
+      port = new_node.edAddOutputPort(p, self.pyobjtype)
+      output_obj_list.append(OutputPort(new_node, port))
+    # create links
+    for k,v in input_values.items():
+      input_port = new_node.getInputPort(k)
+      if isinstance(v, OutputPort):
+        self.proc.edAddLink(v.yacs_port, input_port)
+        self.addCFLink(v.yacs_node, new_node)
+        #self.proc.edAddCFLink(v.yacs_node, new_node)
+      else:
+        input_port.edInitPy(v)
+    # return output ports
+    result = None
+    if len(output_obj_list) == 1 :
+      result = output_obj_list[0]
+    elif len(output_obj_list) > 1 :
+      result = tuple(output_obj_list)
+    return result
+
+  def beginForeach(self, fn_name, input_values):
+    foreach_name = self.newName(fn_name)
+    new_foreach = self.runtime.createForEachLoopDyn(foreach_name,
+                                                    self.pyobjtype)
+    #new_foreach = self.runtime.createForEachLoop(foreach_name, self.pyobjtype)
+    #new_foreach.edGetNbOfBranchesPort().edInitInt(1)
+    self.bloc_stack[-1].edAddChild(new_foreach)
+    bloc_name = "bloc_"+foreach_name
+    new_block = self.runtime.createBloc(bloc_name)
+    new_foreach.edAddChild(new_block)
+    sample_port = new_foreach.edGetSamplePort()
+    input_list_port = new_foreach.edGetSeqOfSamplesPort()
+    if isinstance(input_values, OutputPort):
+      # we need a conversion node pyobj -> seqpyobj
+      conversion_node = self.runtime.createScriptNode("Salome",
+                                                      "input_"+foreach_name)
+      port_name = "val"
+      input_port = conversion_node.edAddInputPort(port_name, self.pyobjtype)
+      output_port = conversion_node.edAddOutputPort(port_name,
+                                                    self.seqpyobjtype)
+      conversion_node.setExecutionMode("local") # no need for container
+      # no script, the same variable for input and output
+      conversion_node.setScript("")
+      self.bloc_stack[-1].edAddChild(conversion_node)
+      self.proc.edAddLink(input_values.yacs_port, input_port)
+      self.addCFLink(input_values.yacs_node, conversion_node)
+      self.proc.edAddLink(output_port, input_list_port)
+      # No need to look for ancestors. Both nodes are on the same level.
+      self.proc.edAddCFLink(conversion_node, new_foreach)
+    else:
+      input_list_port.edInitPy(list(input_values))
+    self.bloc_stack.append(new_foreach)
+    self.bloc_stack.append(new_block)
+    return OutputPort(new_foreach, sample_port)
+
+  def endForeach(self, outputs):
+    self.bloc_stack.pop() # remove the block
+    for_each_node = self.bloc_stack.pop() # remove the foreach
+    converted_ret = None
+    if outputs is not None:
+      # We need a conversion node seqpyobj -> pyobj
+      if type(outputs) is tuple:
+        list_out = list(outputs)
+      else:
+        list_out = [outputs]
+      conversion_node_name = "output_" + for_each_node.getName()
+      conversion_node = self.runtime.createScriptNode("Salome",
+                                                      conversion_node_name)
+      conversion_node.setExecutionMode("local") # no need for container
+      conversion_node.setScript("")
+      self.bloc_stack[-1].edAddChild(conversion_node)
+      list_ret = []
+      idx_name = 0 # for unique port names
+      for port in list_out :
+        if isinstance(port, OutputPort):
+          port_name = port.yacs_port.getName() + "_" + str(idx_name)
+          idx_name += 1
+          input_port = conversion_node.edAddInputPort(port_name,
+                                                      self.seqpyobjtype)
+          output_port = conversion_node.edAddOutputPort(port_name,
+                                                        self.pyobjtype)
+          self.proc.edAddLink(port.yacs_port, input_port)
+          list_ret.append(OutputPort(conversion_node, output_port))
+        else:
+          list_ret.append(port)
+      self.proc.edAddCFLink(for_each_node, conversion_node)
+      if len(list_ret) > 1 :
+        converted_ret = tuple(list_ret)
+      else:
+        converted_ret = list_ret[0]
+    return converted_ret
+
+  def dump(self, file_path):
+    self.proc.saveSchema(file_path)
+
+  def addCFLink(self, node_from, node_to):
+    commonAncestor = self.proc.getLowestCommonAncestor(node_from, node_to)
+    if node_from.getName() != commonAncestor.getName() :
+      link_from = node_from
+      while link_from.getFather().getName() != commonAncestor.getName() :
+        link_from = link_from.getFather()
+      link_to = node_to
+      while link_to.getFather().getName() != commonAncestor.getName() :
+        link_to = link_to.getFather()
+      self.proc.edAddCFLink(link_from, link_to)
+    else:
+      # from node is ancestor of to node. No CF link needed.
+      pass
+
+_generator = None
+
+_default_mode = "Default"
+_yacs_mode = "YACS"
+_exec_mode = _default_mode
+
+def getGenerator():
+  """
+  Get the singleton object.
+  """
+  if this_module._generator is None:
+    if this_module._exec_mode == this_module._yacs_mode:
+      this_module._generator = SchemaGenerator()
+  return this_module._generator
+
+def activateYacsMode():
+  this_module._exec_mode = this_module._yacs_mode
+
+def activateDefaultMode():
+  this_module._exec_mode = this_module._default_mode
+
+def finalize(path):
+  if this_module._exec_mode == this_module._yacs_mode :
+    getGenerator().dump(path)
+
+  
diff --git a/src/py2yacs/yacstools.py b/src/py2yacs/yacstools.py
new file mode 100644 (file)
index 0000000..13b639c
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright (C) 2006-2020  CEA/DEN, EDF R&D
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
+#
+# See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
+#
+def getFunction(script_path, function_name):
+  import os
+  import sys
+  import importlib
+  pathname, filename = os.path.split(script_path)
+  sys.path.append(os.path.abspath(pathname))
+  modname = os.path.splitext(filename)[0]
+  module = importlib.import_module(modname)
+  return getattr(module, function_name)
+