|
| 1 | +# Copyright 2021-2025 MathWorks, Inc. |
| 2 | +""" |
| 3 | +Array interface between Python and MATLAB |
| 4 | + |
| 5 | +This package defines classes and exceptions that create and manage |
| 6 | +multidimensional arrays in Python that are passed between Python and MATLAB. |
| 7 | + |
| 8 | +Modules |
| 9 | +------- |
| 10 | + * mcpyarray - type-specific multidimensional array classes for working |
| 11 | + with MATLAB, implemented in C++ |
| 12 | +""" |
| 13 | + |
| 14 | +import os |
| 15 | +import platform |
| 16 | +import sys |
| 17 | +import warnings |
| 18 | +from pkgutil import extend_path |
| 19 | +__path__ = extend_path(__path__, '__name__') |
| 20 | + |
| 21 | +_package_folder = os.path.dirname(os.path.realpath(__file__)) |
| 22 | +sys.path.append(_package_folder) |
| 23 | + |
| 24 | +# This code allows us to put the proper extern/bin/<arch> directory on the Python path |
| 25 | +# to avoid a situation in which some shared libraries are loaded from a MATLAB while |
| 26 | +# others are loaded from a runtime. The first directory on the path that contains |
| 27 | +# the string "bin/<arch>" (with the proper directory separator) |
| 28 | +# will be checked. If it is "extern/bin/<arch>", it will be used as the |
| 29 | +# extern/bin/<arch> directory. Otherwise, we'll go up two directories and down |
| 30 | +# to extern/bin/<arch>. |
| 31 | +class _MiniPathInitializer(object): |
| 32 | + PLATFORM_DICT = {'Windows': 'PATH', 'Linux': 'LD_LIBRARY_PATH', 'Darwin': 'DYLD_LIBRARY_PATH'} |
| 33 | + |
| 34 | + def __init__(self): |
| 35 | + self.arch = '' |
| 36 | + self.extern_bin_dir = '' |
| 37 | + self.path_var = '' |
| 38 | + self.system = '' |
| 39 | + |
| 40 | + def get_platform_info(self): |
| 41 | + """Ask Python for the platform and architecture.""" |
| 42 | + # This will return 'Windows', 'Linux', or 'Darwin' (for Mac). |
| 43 | + self.system = platform.system() |
| 44 | + if not self.system in _MiniPathInitializer.PLATFORM_DICT: |
| 45 | + raise RuntimeError('{0} is not a supported platform.'.format(self.system)) |
| 46 | + else: |
| 47 | + # path_var is the OS-dependent name of the path variable ('PATH', 'LD_LIBRARY_PATH', "DYLD_LIBRARY_PATH') |
| 48 | + self.path_var = _MiniPathInitializer.PLATFORM_DICT[self.system] |
| 49 | + |
| 50 | + if self.system == 'Windows': |
| 51 | + self.arch = 'win64' |
| 52 | + elif self.system == 'Linux': |
| 53 | + self.arch = 'glnxa64' |
| 54 | + elif self.system == 'Darwin': |
| 55 | + # determine ARM or Intel Mac machine |
| 56 | + if platform.mac_ver()[-1] == 'arm64': |
| 57 | + self.arch = 'maca64' |
| 58 | + else: |
| 59 | + self.arch = 'maci64' |
| 60 | + else: |
| 61 | + raise RuntimeError('Operating system {0} is not supported.'.format(self.system)) |
| 62 | + |
| 63 | + def get_extern_bin_from_path_element_ending_with_runtime_arch(self, dir_found): |
| 64 | + extern_bin_dir = '' |
| 65 | + path_components = dir_found.split(os.sep) |
| 66 | + |
| 67 | + if path_components[-1]: |
| 68 | + last_path_component = path_components[-1] |
| 69 | + possible_extern = -3 |
| 70 | + else: |
| 71 | + # The directory name ended with a slash, so the last item in the list was an empty string. Go back one more. |
| 72 | + last_path_component = path_components[-2] |
| 73 | + possible_extern = -4 |
| 74 | + |
| 75 | + if last_path_component != self.arch: |
| 76 | + output_str = ''.join(('To call deployed MATLAB code on a {0} machine, you must run a {0} version of Python, ', |
| 77 | + 'and your {1} variable must contain an element pointing to "<MR>{2}runtime{2}{0}", ', |
| 78 | + 'where "<MR>" indicates a MATLAB or MATLAB Runtime root. ', |
| 79 | + 'Instead, the value found was as follows: {3}')) |
| 80 | + raise RuntimeError(output_str.format(self.arch, self.path_var, os.sep, dir_found)) |
| 81 | + |
| 82 | + if (len(path_components) + possible_extern) >= 0 and path_components[possible_extern] == 'extern': |
| 83 | + extern_bin_dir = dir_found |
| 84 | + else: |
| 85 | + mroot = os.path.dirname(os.path.dirname(os.path.normpath(dir_found))) |
| 86 | + extern_bin_dir = os.path.join(mroot, 'extern', 'bin', self.arch) |
| 87 | + |
| 88 | + return extern_bin_dir |
| 89 | + |
| 90 | + def get_extern_bin_from_py_sys_path(self): |
| 91 | + #Retrieve Python sys.path as a single string, and search for the substring "extern/bin/<arch>" (with |
| 92 | + #the proper directory separator). If it's already present, assume it's the one we want. |
| 93 | + substr_to_find = os.path.join('extern', 'bin', self.arch) |
| 94 | + for item in sys.path: |
| 95 | + if item.find(substr_to_find) != -1: |
| 96 | + return item |
| 97 | + return '' |
| 98 | + |
| 99 | + def put_extern_bin_on_py_sys_path(self): |
| 100 | + """ |
| 101 | + Look through the system path for the first directory ending with "runtime/<arch>" or |
| 102 | + "bin/<arch>" (with/without trailing slash). Use this to construct a new path ending |
| 103 | + with "extern/bin/<arch>". |
| 104 | + """ |
| 105 | + |
| 106 | + path_elements = [] |
| 107 | + path_elements_orig = '' |
| 108 | + if self.path_var in os.environ: |
| 109 | + path_elements_orig = os.environ[self.path_var] |
| 110 | + # On Windows, some elements of the path may use forward slashes while others use backslashes. |
| 111 | + # Make them all backslashes. |
| 112 | + if self.system == 'Windows': |
| 113 | + path_elements_orig = path_elements_orig.replace('/', '\\') |
| 114 | + path_elements = path_elements_orig.split(os.pathsep) |
| 115 | + if not path_elements: |
| 116 | + if self.system == 'Darwin': |
| 117 | + raise RuntimeError('On the Mac, you must run mwpython rather than python ' + |
| 118 | + 'to start a session or script that imports your package. ' + |
| 119 | + 'For more details, execute "mwpython -help" or see the package documentation.') |
| 120 | + else: |
| 121 | + raise RuntimeError('On {0}, you must set the environment variable "{1}" to a non-empty string. {2}'.format( |
| 122 | + self.system, self.path_var, |
| 123 | + 'For more details, see the package documentation.')) |
| 124 | + |
| 125 | + dir_to_search = os.path.join('runtime', self.arch) |
| 126 | + trailing_substrings_to_find = [dir_to_search, dir_to_search + os.sep] |
| 127 | + |
| 128 | + extern_bin_dir = '' |
| 129 | + for elem in path_elements: |
| 130 | + for trailing_substring in trailing_substrings_to_find: |
| 131 | + if elem.endswith(trailing_substring): |
| 132 | + extern_bin_dir = self.get_extern_bin_from_path_element_ending_with_runtime_arch(elem) |
| 133 | + if extern_bin_dir: |
| 134 | + break |
| 135 | + |
| 136 | + if not extern_bin_dir: |
| 137 | + format_str = 'Could not find an appropriate directory in {0} from which to read binaries. Details::\n{1}' |
| 138 | + raise RuntimeError(format_str.format(self.path_var, path_elements_orig)) |
| 139 | + |
| 140 | + if not os.path.isdir(extern_bin_dir): |
| 141 | + raise RuntimeError('Could not find the directory {0}'.format(extern_bin_dir)) |
| 142 | + self.extern_bin_dir = extern_bin_dir |
| 143 | + sys.path.insert(0, self.extern_bin_dir) |
| 144 | + |
| 145 | +def get_python_version(): |
| 146 | + # UPDATE_IF_PYTHON_VERSION_ADDED_OR_REMOVED : search for this string in codebase |
| 147 | + # when support for a Python version must be added or removed |
| 148 | + _supported_versions = ['3_9', '3_10', '3_11', '3_12', '3_13'] |
| 149 | + _ver = sys.version_info |
| 150 | + _version = '{0}_{1}'.format(_ver[0], _ver[1]) |
| 151 | + newer_than_supported = _ver[1] > 12 |
| 152 | + |
| 153 | + _PYTHONVERSION = None |
| 154 | + |
| 155 | + if _version in _supported_versions: |
| 156 | + _PYTHONVERSION = _version |
| 157 | + elif newer_than_supported: |
| 158 | + warnings.warn('Python versions 3.9, 3.10, 3.11, 3.12, and 3.13 are supported, but your version of Python is %s' % _version) |
| 159 | + _PYTHONVERSION = _version |
| 160 | + else: |
| 161 | + raise EnvironmentError("Python %s is not supported." % _version) |
| 162 | + |
| 163 | + return _PYTHONVERSION |
| 164 | + |
| 165 | +def get_arch_filename(): |
| 166 | + _module_folder = os.path.dirname(os.path.realpath(__file__)) |
| 167 | + _arch_filename = os.path.join(_module_folder, 'engine', '_arch.txt') |
| 168 | + return _arch_filename |
| 169 | + |
| 170 | +def add_dll_dir_on_win(arch, dir_to_add): |
| 171 | + if arch == 'win64': |
| 172 | + if not dir_to_add: |
| 173 | + raise RuntimeError('Cannot add empty DLL directory') |
| 174 | + os.add_dll_directory(dir_to_add) |
| 175 | + |
| 176 | +def get_dirs_from_arch_file_without_import(): |
| 177 | + _PYTHONVERSION = get_python_version() |
| 178 | + _arch_filename = get_arch_filename() |
| 179 | + firstExceptionMessage = '' |
| 180 | + if not os.path.isfile(_arch_filename): |
| 181 | + return False |
| 182 | + |
| 183 | + try: |
| 184 | + _arch_file = open(_arch_filename,'r') |
| 185 | + _lines = _arch_file.readlines() |
| 186 | + [_arch, _bin_dir,_engine_dir, _extern_bin_dir] = [x.rstrip() for x in _lines if x.rstrip() != ""] |
| 187 | + _arch_file.close() |
| 188 | + sys.path.insert(0,_engine_dir) |
| 189 | + sys.path.insert(0,_extern_bin_dir) |
| 190 | + add_dll_dir_on_win(_arch, _bin_dir) |
| 191 | + except Exception as exc: |
| 192 | + firstExceptionMessage = 'Please contact MathWorks Technical Support for assistance:\nDetails: {}'.format( |
| 193 | + exc) |
| 194 | + |
| 195 | + if firstExceptionMessage: |
| 196 | + return False |
| 197 | + else: |
| 198 | + return True |
| 199 | + |
| 200 | +def subdir_exists(folder_name, subfolder_name): |
| 201 | + return os.path.exists(os.path.join(folder_name, subfolder_name)) |
| 202 | + |
| 203 | + |
| 204 | +_mpi = _MiniPathInitializer() |
| 205 | +_mpi.get_platform_info() |
| 206 | +extern_bin_dir = '' |
| 207 | +if subdir_exists(_package_folder, 'engine'): |
| 208 | + success = get_dirs_from_arch_file_without_import() |
| 209 | + if not success: |
| 210 | + if 'MWE_INSTALL' in os.environ: |
| 211 | + mroot_from_env_var = os.environ['MWE_INSTALL'] |
| 212 | + extern_bin_dir = os.path.join(mroot_from_env_var, 'extern', 'bin', _mpi.arch) |
| 213 | + if not os.path.exists(extern_bin_dir): |
| 214 | + raise RuntimeError('directory {} does not exist'.format(extern_bin_dir)) |
| 215 | + sys.path.insert(0, extern_bin_dir) |
| 216 | + bin_dir = extern_bin_dir.replace('extern' + os.sep + 'bin', 'bin') |
| 217 | + add_dll_dir_on_win(_mpi.arch, bin_dir) |
| 218 | + else: |
| 219 | + raise RuntimeError('unable to read {}'.format(get_arch_filename())) |
| 220 | +else: |
| 221 | + extern_bin_dir = _mpi.get_extern_bin_from_py_sys_path() |
| 222 | + if not extern_bin_dir: |
| 223 | + _mpi.put_extern_bin_on_py_sys_path() |
| 224 | + extern_bin_dir = _mpi.extern_bin_dir |
| 225 | + |
| 226 | + bin_dir = extern_bin_dir.replace('extern' + os.sep + 'bin', 'bin') |
| 227 | + add_dll_dir_on_win(_mpi.arch, bin_dir) |
| 228 | + |
| 229 | + |
| 230 | +from matlabmultidimarrayforpython import double, single, uint8, int8, uint16, \ |
| 231 | + int16, uint32, int32, uint64, int64, logical, ShapeError, SizeError |
| 232 | + |
0 commit comments