diff --git a/modules/python/bindings/visp/__init__.py b/modules/python/bindings/visp/__init__.py index b08a067c84..de44b785e2 100644 --- a/modules/python/bindings/visp/__init__.py +++ b/modules/python/bindings/visp/__init__.py @@ -40,15 +40,21 @@ # print(sys.path) -# On windows, we need to explicitely add paths where Python should look for DLLs (This starts with Python >= 3.8) -LOADER_DIR = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) - - - - - -import _visp -from _visp import * +try: + import _visp + from _visp import * +except ImportError: + import platform + if platform.system() == "Windows": # On windows import can fail because DLLs are not found in the default search paths + from .windows_dll_manager import get_dll_paths, build_directory_manager + # Use the context to clean up the PATH/dll directories after the import (no namespace pollution) + with build_directory_manager() as dll_dir_manager: + for p in get_dll_paths(): + dll_dir_manager.add_dll_directory(p) + import _visp + from _visp import * + else: + raise # Fake module names for k in _visp.__dict__: diff --git a/modules/python/bindings/visp/windows-dll-manager.py b/modules/python/bindings/visp/windows-dll-manager.py new file mode 100644 index 0000000000..0f1a286abd --- /dev/null +++ b/modules/python/bindings/visp/windows-dll-manager.py @@ -0,0 +1,75 @@ +''' +This code is directly adapted from proxsuite_nlp, see: + +- https://github.com/Simple-Robotics/proxsuite-nlp/blob/main/bindings/python/proxsuite_nlp/windows_dll_manager.py +- https://github.com/Simple-Robotics/proxsuite-nlp/blob/main/bindings/python/proxsuite_nlp/__init__.py + +On windows, since Python 3.8, dll directories must be explicetly specified (cannot go through path), see +- https://docs.python.org/3/library/os.html#os.add_dll_directory + +''' + + +import os +import sys +import contextlib + + +def get_dll_paths(): + # Assumes that we are in a conda environment, and that ViSP DLLs and the dependencies are installed in this environment + # For the choice of defaults: see https://peps.python.org/pep-0250/#implementation + DEFAULT_DLL_PATHS = [ + '..\\..\\..\\..\\bin', # when current folder is lib/python-version/site-packages/package + '..\\..\\..\\bin', # when current folder is lib/site-packages/package + ] + # If we have a different setup, the user should specify their own paths + visp_user_defined_dll_paths = os.getenv("VISP_WINDOWS_DLL_PATH") + if visp_user_defined_dll_paths is None: + return [ + os.path.join(os.path.dirname(__file__), dll_path) for dll_path in DEFAULT_DLL_PATHS + ] + else: + return visp_user_defined_dll_paths.split(os.pathsep) + + +class PathManager(contextlib.AbstractContextManager): + """Restore PATH state after importing Python module""" + + def add_dll_directory(self, dll_dir: str): + os.environ["PATH"] += os.pathsep + dll_dir + + def __enter__(self): + self.old_path = os.environ["PATH"] + return self + + def __exit__(self, *exc_details): + os.environ["PATH"] = self.old_path + + +class DllDirectoryManager(contextlib.AbstractContextManager): + """Restore DllDirectory state after importing Python module""" + + def add_dll_directory(self, dll_dir: str): + # add_dll_directory can fail on relative path and non + # existing path. + # Since we don't know all the fail criterion we just ignore + # thrown exception + try: + self.dll_dirs.append(os.add_dll_directory(dll_dir)) + except OSError: + pass + + def __enter__(self): + self.dll_dirs = [] + return self + + def __exit__(self, *exc_details): + for d in self.dll_dirs: + d.close() + + +def build_directory_manager(): + if sys.version_info >= (3, 8): + return DllDirectoryManager() + else: # Below 3.8, the path variable is used to search for DLLs + return PathManager()