|
1 |
| -# Copyright 2022 MathWorks, Inc. |
| 1 | +# Copyright 2021 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 | +The arrays, while similar to Python sequences, have different behaviors. |
| 8 | +
|
| 9 | +Modules |
| 10 | +------- |
| 11 | + * mlarray - type-specific multidimensional array classes for working |
| 12 | + with MATLAB, implemented in Python |
| 13 | + * mcpyarray - type-specific multidimensional array classes for working |
| 14 | + with MATLAB, implemented in C++ |
| 15 | + * mlexceptions - exceptions raised when manipulating mlarray objects |
| 16 | +""" |
2 | 17 |
|
3 | 18 | import os
|
4 |
| -import platform |
5 | 19 | import sys
|
6 |
| -import pkgutil |
7 | 20 |
|
8 |
| -__path__ = pkgutil.extend_path(__path__, __name__) |
9 |
| -package_folder = os.path.dirname(os.path.realpath(__file__)) |
10 |
| -sys.path.append(package_folder) |
| 21 | +# These can be removed once we no longer use _MiniPathInitializer. |
| 22 | +import platform |
| 23 | +import re |
| 24 | + |
| 25 | +from pkgutil import extend_path |
| 26 | +__path__ = extend_path(__path__, '__name__') |
11 | 27 |
|
12 |
| -def add_dirs_to_path(bin_dir, engine_dir, extern_dir): |
13 |
| - """ |
14 |
| - Adds MATLAB engine and extern/bin directories to sys.path. |
15 |
| - """ |
16 |
| - path = 'PATH' |
| 28 | +_package_folder = os.path.dirname(os.path.realpath(__file__)) |
| 29 | +sys.path.append(_package_folder) |
17 | 30 |
|
18 |
| - if not os.path.isdir(engine_dir): |
19 |
| - raise RuntimeError("Could not find directory: {0}".format(engine_dir)) |
20 |
| - |
21 |
| - if not os.path.isdir(extern_dir): |
22 |
| - raise RuntimeError("Could not find directory: {0}".format(extern_dir)) |
| 31 | +# This code allows us to: |
| 32 | +# (1) switch from a pure Python ("mlarray") to a C++ extension ("mcpyarray") |
| 33 | +# implementation by setting the environment variable USE_MCPYARRAY to 1 |
| 34 | +# (2) put the proper extern/bin/<arch> directory on the Python path to avoid |
| 35 | +# a situation in which some shared libraries are loaded from a MATLAB while |
| 36 | +# others are loaded from a runtime. The first directory on the path that contains |
| 37 | +# the string "bin/<arch>" (with the proper directory separator) |
| 38 | +# will be checked. If it is "extern/bin/<arch>", it will be used as the |
| 39 | +# extern/bin/<arch> directory. Otherwise, we'll go up two directories and down |
| 40 | +# to extern/bin/<arch>. |
| 41 | +class _MiniPathInitializer(object): |
| 42 | + PLATFORM_DICT = {'Windows': 'PATH', 'Linux': 'LD_LIBRARY_PATH', 'Darwin': 'DYLD_LIBRARY_PATH'} |
| 43 | + |
| 44 | + def __init__(self): |
| 45 | + self.arch = '' |
| 46 | + self.extern_bin_dir = '' |
| 47 | + self.path_var = '' |
| 48 | + self.system = '' |
| 49 | + self.use_mcpyarray = False |
| 50 | + if os.environ.get('USE_MCPYARRAY') and os.environ['USE_MCPYARRAY'] == '1': |
| 51 | + self.use_mcpyarray = True |
| 52 | + |
| 53 | + def get_platform_info(self): |
| 54 | + """Ask Python for the platform and architecture.""" |
| 55 | + # This will return 'Windows', 'Linux', or 'Darwin' (for Mac). |
| 56 | + self.system = platform.system() |
| 57 | + if not self.system in _MiniPathInitializer.PLATFORM_DICT: |
| 58 | + raise RuntimeError('{0} is not a supported platform.'.format(self.system)) |
| 59 | + else: |
| 60 | + # path_var is the OS-dependent name of the path variable ('PATH', 'LD_LIBRARY_PATH', "DYLD_LIBRARY_PATH') |
| 61 | + self.path_var = _MiniPathInitializer.PLATFORM_DICT[self.system] |
| 62 | + |
| 63 | + if self.system == 'Windows': |
| 64 | + self.arch = 'win64' |
| 65 | + elif self.system == 'Linux': |
| 66 | + self.arch = 'glnxa64' |
| 67 | + elif self.system == 'Darwin': |
| 68 | + self.arch = 'maci64' |
| 69 | + else: |
| 70 | + raise RuntimeError('Operating system {0} is not supported.'.format(self.system)) |
| 71 | + |
| 72 | + def is_extern_bin_on_py_sys_path(self): |
| 73 | + #Retrieve Python sys.path as a single string, and search for the substring "extern/bin/<arch>" (with |
| 74 | + #the proper directory separator). If it's already present, assume it's the one we want. |
| 75 | + substr_to_find = os.path.join('extern', 'bin', self.arch) |
| 76 | + for item in sys.path: |
| 77 | + if item.find(substr_to_find) != -1: |
| 78 | + return True |
| 79 | + return False |
23 | 80 |
|
24 |
| - if platform.system() == 'Windows': |
25 |
| - if not os.path.isdir(bin_dir): |
26 |
| - raise RuntimeError("Could not find directory: {0}".format(bin_dir)) |
27 |
| - if path in os.environ: |
28 |
| - paths = os.environ[path] |
29 |
| - os.environ[path] = bin_dir + os.pathsep + paths |
| 81 | + def put_extern_bin_on_py_sys_path(self): |
| 82 | + """ |
| 83 | + Look through the system path for the first directory ending with "runtime/<arch>" or |
| 84 | + "bin/<arch>" (with/without trailing slash). Use this to construct a new path ending |
| 85 | + with "extern/bin/<arch>". |
| 86 | + """ |
| 87 | + |
| 88 | + path_elements = [] |
| 89 | + if self.path_var in os.environ: |
| 90 | + path_elements_orig = os.environ[self.path_var] |
| 91 | + # On Windows, some elements of the path may use forward slashes while others use backslashes. |
| 92 | + # Make them all backslashes. |
| 93 | + if self.system == 'Windows': |
| 94 | + path_elements_orig = path_elements_orig.replace('/', '\\') |
| 95 | + path_elements = path_elements_orig.split(os.pathsep) |
| 96 | + if not path_elements: |
| 97 | + if self.system == 'Darwin': |
| 98 | + raise RuntimeError('On the Mac, you must run mwpython rather than python ' + |
| 99 | + 'to start a session or script that imports your package. ' + |
| 100 | + 'For more details, execute "mwpython -help" or see the package documentation.') |
30 | 101 | else:
|
31 |
| - os.environ[path] = bin_dir |
32 |
| - if sys.version_info.major >= 3 and sys.version_info.minor >= 8: |
33 |
| - os.add_dll_directory(bin_dir) |
| 102 | + raise RuntimeError('On {0}, you must set the environment variable "{1}" to a non-empty string. {2}'.format( |
| 103 | + self.system, self.path_var, |
| 104 | + 'For more details, see the package documentation.')) |
| 105 | + |
| 106 | + dir_to_search = os.path.join('runtime', self.arch) |
| 107 | + trailing_substrings_to_find = [dir_to_search, dir_to_search + os.sep] |
34 | 108 |
|
35 |
| - sys.path.insert(0, engine_dir) |
36 |
| - sys.path.insert(0, extern_dir) |
| 109 | + dir_found = '' |
| 110 | + for elem in path_elements: |
| 111 | + for trailing_substring in trailing_substrings_to_find: |
| 112 | + if elem.endswith(trailing_substring): |
| 113 | + dir_found = elem |
| 114 | + break |
| 115 | + if dir_found: |
| 116 | + break |
37 | 117 |
|
38 |
| -arch_file = os.path.join(package_folder, 'engine', '_arch.txt') |
39 |
| -if not os.path.isfile(arch_file): |
40 |
| - raise RuntimeError("The MATLAB Engine for Python install is corrupted, please try to re-install.") |
| 118 | + if not dir_found: |
| 119 | + raise RuntimeError('Could not find an appropriate directory in {0} from which to read binaries.'.format( |
| 120 | + self.path_var)) |
41 | 121 |
|
42 |
| -with open(arch_file, 'r') as root: |
43 |
| - [arch, bin_folder, engine_folder, extern_bin] = [line.strip() for line in root.readlines()] |
| 122 | + path_components = dir_found.split(os.sep) |
| 123 | + |
| 124 | + if path_components[-1]: |
| 125 | + last_path_component = path_components[-1] |
| 126 | + possible_extern = -3 |
| 127 | + else: |
| 128 | + # The directory name ended with a slash, so the last item in the list was an empty string. Go back one more. |
| 129 | + last_path_component = path_components[-2] |
| 130 | + possible_extern = -4 |
44 | 131 |
|
| 132 | + if last_path_component != self.arch: |
| 133 | + output_str = ''.join(('To call deployed MATLAB code on a {0} machine, you must run a {0} version of Python, ', |
| 134 | + 'and your {1} variable must contain an element pointing to "<MR>{2}runtime{2}{0}", ', |
| 135 | + 'where "<MR>" indicates a MATLAB or MATLAB Runtime root. ', |
| 136 | + 'Instead, the value found was as follows: {3}')) |
| 137 | + raise RuntimeError(output_str.format(self.arch, self.path_var, os.sep, dir_found)) |
45 | 138 |
|
46 |
| -add_dirs_to_path(bin_folder, engine_folder, extern_bin) |
| 139 | + if (len(path_components) + possible_extern) >= 0 and path_components[possible_extern] == 'extern': |
| 140 | + extern_bin_dir = dir_found |
| 141 | + else: |
| 142 | + mroot = os.path.dirname(os.path.dirname(os.path.normpath(dir_found))) |
| 143 | + extern_bin_dir = os.path.join(mroot, 'extern', 'bin', self.arch) |
| 144 | + if not os.path.isdir(extern_bin_dir): |
| 145 | + raise RuntimeError('Could not find the directory {0}'.format(extern_bin_dir)) |
| 146 | + self.extern_bin_dir = extern_bin_dir |
| 147 | + sys.path.insert(0, self.extern_bin_dir) |
47 | 148 |
|
48 |
| -from matlabmultidimarrayforpython import double, single, uint8, int8, uint16, \ |
49 |
| - int16, uint32, int32, uint64, int64, logical, ShapeError, SizeError |
| 149 | +_mpi = _MiniPathInitializer() |
| 150 | +if _mpi.use_mcpyarray: |
| 151 | + _mpi.get_platform_info() |
| 152 | + if not _mpi.is_extern_bin_on_py_sys_path(): |
| 153 | + _mpi.put_extern_bin_on_py_sys_path() |
| 154 | + from matlabmultidimarrayforpython import double, single, uint8, int8, uint16, \ |
| 155 | + int16, uint32, int32, uint64, int64, logical, ShapeError, SizeError |
| 156 | +else: |
| 157 | + from mlarray import double, single, uint8, int8, uint16, \ |
| 158 | + int16, uint32, int32, uint64, int64, logical |
| 159 | + from mlexceptions import ShapeError as ShapeError |
| 160 | + from mlexceptions import SizeError as SizeError |
0 commit comments