Source code for Sofa

"""
All SOFA key components supported by the SOFA consortium

Example:
  .. code-block:: python

    import Sofa.Core
    import Sofa.Simulation
    import SofaRuntime
    SofaRuntime.importPlugin("Sofa.Component")

    n = Sofa.Core.Node("MyNode")
    n.addChild("Node2")
    n.addObject("MechanicalObject", name="dofs")

    Sofa.Simulation.init(n)
    Sofa.Simulation.print(n)

"""

import sys
import os
import inspect
import functools
import traceback
import importlib

print("---------------------------------------")
print("Checking SOFA_ROOT and SOFAPYTHON3_ROOT")

# check if SOFA_ROOT has been (well) set
sofa_root = os.environ.get('SOFA_ROOT')
if sofa_root:
    print("Using environment variable SOFA_ROOT: " + sofa_root)
else:
    print("Warning: environment variable SOFA_ROOT is empty. Trying to guess it.")
    # try a guess from <sofa_root>/plugins/SofaPython3/lib/python3/site-packages/Sofa
    sofa_root_guess = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/../../../../../..')
    if os.path.isdir(os.path.abspath(sofa_root_guess + '/lib' )):
        print("Guessed SOFA_ROOT: " + sofa_root_guess)
        sofa_root = sofa_root_guess
        os.environ["SOFA_ROOT"] = sofa_root
    else:
        print("Warning: cannot guess SOFA_ROOT",
              "Loading SOFA libraries will likely fail and/or SOFA won't find its resources.")

if sofa_root and sys.platform == 'win32':

    # check if SOFAPYTHON3_ROOT has been (well) set, only useful for Windows
    sofapython3_root = os.environ.get('SOFAPYTHON3_ROOT')
    if sofapython3_root:
        print("Using environment variable SOFAPYTHON3_ROOT: " + sofapython3_root)
    else:
        print("Warning: environment variable SOFAPYTHON3_ROOT is empty. Trying to guess it.")
        # try a guess from <sofapython3_root>/lib/python3/site-packages/Sofa
        sofapython3_root_guess = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + '/../../../..')
        if os.path.isdir(os.path.abspath(sofapython3_root_guess + '/lib' )):
            print("Guessed SOFAPYTHON3_ROOT: " + sofapython3_root_guess)
            sofapython3_root = sofapython3_root_guess
            os.environ["SOFAPYTHON3_ROOT"] = sofapython3_root
        else:
            print("Warning: cannot guess SOFAPYTHON3_ROOT",
                  "Loading SofaPython3 modules will likely fail.")

    # Windows-only: starting from python 3.8, python wont read the env. variable PATH to get SOFA's dlls.
    # os.add_dll_directory() is the new way to add paths for python to get external libraries.
    sofa_bin_path = os.path.join(sofa_root, "bin")
    sofapython3_bin_path = os.path.join(sofapython3_root, "bin")

    compilation_modes = ["Release", "RelWithDebInfo", "Debug", "MinSizeRel"]
    sofa_bin_compilation_modes = []
    sofapython3_bin_compilation_modes = []
    for mode in compilation_modes:
        if os.path.isdir(os.path.abspath(os.path.join(sofa_bin_path, mode))):
            sofa_bin_compilation_modes.append(os.path.join(sofa_bin_path, mode))
        if os.path.isdir(os.path.abspath(os.path.join(sofapython3_bin_path, mode))):
            sofapython3_bin_compilation_modes.append(os.path.join(sofapython3_bin_path, mode))

    if sofa_bin_compilation_modes:
        print("Detected SOFA development build")
    if sofapython3_bin_compilation_modes:
        print("Detected SofaPython3 development build")

    sofa_bin_candidates = [sofa_bin_path] + sofa_bin_compilation_modes
    sofapython3_bin_candidates = [sofapython3_bin_path] + sofapython3_bin_compilation_modes

    for candidate in sofa_bin_candidates:
        sofa_file_test = os.path.join(candidate, "Sofa.Helper.dll")
        if os.path.isfile(sofa_file_test):
            print("Found Sofa.Helper.dll in " + candidate)
            sofa_bin_path = candidate
            break

    sofa_file_test = os.path.join(sofa_bin_path, "Sofa.Helper.dll")

    for candidate in sofapython3_bin_candidates:
        sofapython3_file_test = os.path.join(candidate, "SofaPython3.dll")
        if os.path.isfile(sofapython3_file_test):
            print("Found SofaPython3.dll in " + candidate)
            sofapython3_bin_path = candidate
            break

    sofapython3_file_test = os.path.join(sofapython3_bin_path, "SofaPython3.dll")

    if not os.path.isfile(sofa_file_test):
        print("Warning: environment variable SOFA_ROOT is set but seems invalid.",
              "Loading SOFA libraries will likely fail.")
        print("SOFA_ROOT is currently: " + sofa_root)
    if not os.path.isfile(sofapython3_file_test):
        print("Warning: cannot find SofaPython3.dll at path: " + sofapython3_bin_path)
        print("This path will NOT be added to the DLL search path.",
              "Loading SofaPython3 python modules will likely fail.")

    if sys.version_info.minor >= 8:
        # Starting from python3.8 we need to explicitly find SOFA libraries
        if os.path.isfile(sofa_file_test):
            os.add_dll_directory(sofa_bin_path)
        if os.path.isfile(sofapython3_file_test):
            os.add_dll_directory(sofapython3_bin_path)
    else:
        # Add temporarily the bin/lib path to the env variable PATH
        if os.path.isfile(sofa_file_test):
            os.environ['PATH'] = sofa_bin_path + os.pathsep + os.environ['PATH']
        if os.path.isfile(sofapython3_file_test):
            os.environ['PATH'] = sofapython3_bin_path + os.pathsep + os.environ['PATH']

print("---------------------------------------")
sys.stdout.flush()

import Sofa.constants
import Sofa.Helper
import Sofa.Core
import Sofa.Simulation
import Sofa.Types
import Sofa.Components
import SofaTypes

from .prefab import *

__all__=["constants", "Helper", "Core", "Simulation", "Types", "SofaTypes", "prefab"]

# Keep a list of the modules always imported in the Sofa-PythonEnvironment
try:
    __SofaPythonEnvironment_importedModules__
except:
    __SofaPythonEnvironment_importedModules__ = sys.modules.copy()

    # some modules could be added here manually and can be modified procedurally
    # e.g. plugin's modules defined from c++
    __SofaPythonEnvironment_modulesExcludedFromReload = []


def unloadModules():
    """ call this function to unload python modules and to force their reload
        (useful to take into account their eventual modifications since
        their last import).
    """
    global __SofaPythonEnvironment_importedModules__
    toremove = [name for name in sys.modules if not name in __SofaPythonEnvironment_importedModules__ and not name in __SofaPythonEnvironment_modulesExcludedFromReload ]
    for name in toremove:
        del(sys.modules[name]) # unload it


def formatStackForSofa(o):
    """ format the stack trace provided as a parameter into a string like that:
        in filename.py:10:functioname()
          -> the line of code.
        in filename2.py:101:functioname1()
            -> the line of code.
        in filename3.py:103:functioname2()
              -> the line of code.
    """
    ss='At: '
    for entry in o:
        ss+= '\n  '+ str(entry[1]) + '(' + str(entry[2]) + '): '+ entry[3]
    return ss + "\n"

def getStackForSofa():
    """returns the current stack with a "informal" formatting. """
    ## we exclude the first level in the stack because it is the getStackForSofa() function itself.
    ss=inspect.stack()[1:]
    return formatStackForSofa(ss)


def getPythonCallingPointAsString():
    """returns the last entry with an "informal" formatting. """

    ## we exclude the first level in the stack because it is the getStackForSofa() function itself.
    ss=inspect.stack()[1:2]
    return formatStackForSofa(ss)


def getPythonCallingPoint():
    """returns the tuple with closest filename & line. """
    ## we exclude the first level in the stack because it is the getStackForSofa() function itself.
    ss=inspect.stack()[1:2]
    tmp=(os.path.abspath(ss[1]), ss[2])
    return tmp


def sendMessageFromException(e):
    exc_type, exc_value, exc_tb = sys.exc_info()
    sofaExceptHandler(exc_type, exc_value, exc_tb)


def sofaFormatHandler(type, value, tb):
    global oldexcepthook
    """This exception handler, convert python exceptions & traceback into more classical sofa error messages of the form:
       Message Description
       Python Stack (most recent are at the end)
          File file1.py line 4  ...
          File file1.py line 10 ...
          File file1.py line 40 ...
          File file1.py line 23 ...
            faulty line
    """
    s="\nPython Stack (most recent are at the end): \n"
    for line in traceback.format_tb(tb):
        s += line

    return repr(value)+"\n"+s


def getSofaFormattedStringFromException(e):
    exc_type, exc_value, exc_tb = sys.exc_info()
    return sofaFormatHandler(exc_type, exc_value, exc_tb)

def sofaExceptHandler(type, value, tb):
    global oldexcepthook
    """This exception handler converts python exceptions & traceback into classical SOFA error messages

       Message:

        .. code-block:: text

            Python Stack (most recent are at the end)
            File file1.py line 4  ...
            File file1.py line 10 ...
            File file1.py line 40 ...
            File file1.py line 23 ...

    """
    h = type.__name__

    if str(value) != '':
        h += ': ' + str(value)

    s = ''.join(traceback.format_tb(tb))

    Sofa.Helper.msg_error(h + '\n' + s, "line", 7)

sys.excepthook=sofaExceptHandler

def pyType2sofaType(v):
    if isinstance(v, bool):
        return "bool"
    if isinstance(v, str):
        return "string"
    if isinstance(v, int):
        return "int"
    if isinstance(v, float):
        return "double"
    if isinstance(v, list) and len(v)==3:
        return "Vec3d"
    if isinstance(v, list):
        return "vector<double>"
    if isinstance(v, Sofa.PyTypes.DataType):
        return v.sofaTypeName
    return None


[docs]def msg_error(target, message): """API emitting error messages """ frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) Sofa.Helper.msg_error(target, message, frameinfo.filename, frameinfo.lineno)
[docs]def msg_info(target, message): """API emitting information messages """ frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) Sofa.Helper.msg_info(target, message, frameinfo.filename, frameinfo.lineno)
[docs]def msg_warning(target, message): """API emitting warning messages """ frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) Sofa.Helper.msg_warning(target, message, frameinfo.filename, frameinfo.lineno)
[docs]def msg_deprecated(target, message): """API emitting deprecation messages """ frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) Sofa.Helper.msg_deprecated(target, message, frameinfo.filename, frameinfo.lineno)
import inspect def PrefabBuilder(f): frameinfo = inspect.getframeinfo(inspect.currentframe().f_back) definedloc = (frameinfo.filename, frameinfo.lineno) def SofaPrefabF(*args, **kwargs): class InnerSofaPrefab(Sofa.Core.RawPrefab): def __init__(self, *args, **kwargs): Sofa.Core.RawPrefab.__init__(self, *args, **kwargs) self.isValid = True def doReInit(self): if not self.isValid: return try: argnames = inspect.getfullargspec(f).args kkwargs = {} kkwargs["self"] = self for name in argnames[:]: if name != "self": kkwargs[name] = self.__data__[name].value self.cb(**kkwargs) except Exception as e: self.isValid = False exc_type, exc_value, exc_tb = sys.exc_info() Sofa.Helper.msg_error(self, "Unable to build prefab \n "+getSofaFormattedStringFromException(e)) try: selfnode = None kwargs["name"] = kwargs.get("name", f.__code__.co_name) selfnode = InnerSofaPrefab(*args, **kwargs) selfnode.setDefinitionSourceFileName(definedloc[0]) selfnode.setDefinitionSourceFilePos(definedloc[1]) selfnode.setSourceTracking(definedloc[0]) selfnode.cb = f ## retrieve meta data from decorated class: selfnode.addData(name="prefabname", value=f.__code__.co_name, type="string", help="The prefab's name", group="Infos") selfnode.addData(name="docstring", value=f.__doc__, type="string", help="This prefab's docstring", group="Infos") ## Now we retrieve all params passed to the prefab and add them as datafields: argnames = inspect.getfullargspec(f).args defaults = inspect.getfullargspec(f).defaults if argnames is None: argnames = [] defaults = [] if defaults is None: defaults = [] i = len(argnames) - len(defaults) for n in range(0, len(defaults)): if argnames[i+n] not in selfnode.__data__: if pyType2sofaType(defaults[n]) != None: selfnode.addPrefabParameter(name=argnames[i+n], default=kwargs.get(argnames[i+n], defaults[n]), type=pyType2sofaType(defaults[n]), help="Undefined") else: Sofa.Helper.msg_error("Missing type for parameters: ", argnames[i+n]) selfnode.init() except Exception as e: if selfnode is not None: selfnode.isValid=False Sofa.Helper.msg_error(selfnode, "Unable to create prefab because: "+getSofaFormattedStringFromException(e)) else: Sofa.Helper.msg_error("PrefabBuilder", "Unable to create prefab because: "+getSofaFormattedStringFromException(e)) return selfnode SofaPrefabF.__dict__["__original__"] = f return SofaPrefabF