diff --git a/mpython/_version.py b/mpython/_version.py index 364900b..7824322 100644 --- a/mpython/_version.py +++ b/mpython/_version.py @@ -1 +1 @@ -__version__ = "25.04alpha3" +__version__ = "25.04rc1" diff --git a/mpython/array.py b/mpython/array.py index b6d4bf7..0aad40a 100644 --- a/mpython/array.py +++ b/mpython/array.py @@ -1,7 +1,11 @@ import numpy as np from .core import WrappedArray, _ListishMixin -from .utils import _copy_if_needed +from .utils import _copy_if_needed, DelayedImport + + +class _imports(DelayedImport): + Cell = 'mpython.cell.Cell' class Array(_ListishMixin, WrappedArray): @@ -48,7 +52,7 @@ def _as_runtime(self) -> np.ndarray: return np.ndarray.view(self, np.ndarray) @classmethod - def _from_runtime(cls, other) -> "Array": + def _from_runtime(cls, other, runtime=None) -> "Array": other = np.asarray(other) if len(other.shape) == 2 and other.shape[0] == 1: other = other.squeeze(0) @@ -176,7 +180,7 @@ def from_cell(cls, other, **kwargs) -> "Array": array : Array Converted array. """ - from .cell import Cell # FIXME: avoid circular import + Cell = _imports.Cell if not isinstance(other, Cell): raise TypeError(f"Expected a {Cell} but got a {type(other)}") diff --git a/mpython/cell.py b/mpython/cell.py index d134f25..b5d3761 100644 --- a/mpython/cell.py +++ b/mpython/cell.py @@ -1,9 +1,9 @@ import numpy as np -from .core import AnyDelayedArray, DelayedCell, MatlabType, WrappedArray, _ListMixin -from .utils import _copy_if_needed, _empty_array, _import_matlab, _matlab_array_types - -global matlab +from .core import ( + AnyDelayedArray, DelayedCell, MatlabType, WrappedArray, _ListMixin +) +from .utils import _copy_if_needed, _empty_array, _matlab_array_types class Cell(_ListMixin, WrappedArray): @@ -67,7 +67,8 @@ class Cell(_ListMixin, WrappedArray): def _DEFAULT(cls, shape: list = ()) -> np.ndarray: data = np.empty(shape, dtype=object) opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["writeonly", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["writeonly", "no_broadcast"] ) with np.nditer(data, **opt) as iter: for elem in iter: @@ -77,7 +78,8 @@ def _DEFAULT(cls, shape: list = ()) -> np.ndarray: def _fill_default(self): arr = np.ndarray.view(self, np.ndarray) opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["writeonly", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["writeonly", "no_broadcast"] ) with np.nditer(arr, **opt) as iter: for elem in iter: @@ -106,7 +108,7 @@ def _as_runtime(self) -> dict: return dict(type__="cell", size__=size, data__=data) @classmethod - def _from_runtime(cls, objdict: dict) -> "Cell": + def _from_runtime(cls, objdict: dict, runtime=None) -> "Cell": if isinstance(objdict, (list, tuple, set)): shape = [len(objdict)] objdict = dict(type__="cell", size__=shape, data__=objdict) @@ -126,16 +128,18 @@ def _from_runtime(cls, objdict: dict) -> "Cell": obj = data.view(cls) except Exception: raise RuntimeError( - f"Failed to construct Cell data:\n data={data}\n objdict={objdict}" + f"Failed to construct Cell data:\n" + f" data={data}\n objdict={objdict}" ) # recurse opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["readwrite", "no_broadcast"] ) with np.nditer(data, **opt) as iter: for elem in iter: - elem[()] = MatlabType._from_runtime(elem.item()) + elem[()] = MatlabType._from_runtime(elem.item(), runtime) return obj @@ -217,10 +221,6 @@ def from_any(cls, other, **kwargs) -> "Cell": # recursive shallow conversion if not deepcat: - # make sure matlab is imported so that we can detect - # matlab arrays. - _import_matlab() - # This is so list[list] are converted to Cell[Cell] and # not to a 2D Cell array. def asrecursive(other): @@ -266,7 +266,8 @@ def asrecursive(other): # recurse opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["readwrite", "no_broadcast"] ) with np.nditer(other, **opt) as iter: for elem in iter: @@ -286,7 +287,8 @@ def _unroll_build(cls, arr): rebuild = False arr = np.asarray(arr) opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["readwrite", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["readwrite", "no_broadcast"] ) with np.nditer(arr, **opt) as iter: for elem in iter: diff --git a/mpython/core/base_types.py b/mpython/core/base_types.py index 6cf3e50..26fd942 100644 --- a/mpython/core/base_types.py +++ b/mpython/core/base_types.py @@ -2,7 +2,17 @@ import numpy as np -from ..utils import _import_matlab, _matlab_array_types +from ..utils import _import_matlab, _matlab_array_types, DelayedImport + + +class _imports(DelayedImport): + Array = 'mpython.array.Array' + Cell = 'mpython.cell.Cell' + Struct = 'mpython.struct.Struct' + SparseArray = 'mpython.sparse_array.SparseArray' + MatlabClass = 'mpython.matlab_class.MatlabClass' + MatlabFunction = 'mpython.matlab_function.MatlabFunction' + AnyDelayedArray = 'mpython.core.delayed_types.AnyDelayedArray' class MatlabType: @@ -16,16 +26,14 @@ def from_any(cls, other, **kwargs): !!! warning "Conversion is performed in-place when possible." """ - # FIXME: Circular import - from ..array import Array - - # FIXME: Circular import - from ..cell import Cell - from ..matlab_class import MatlabClass - from ..matlab_function import MatlabFunction - from ..sparse_array import SparseArray - from ..struct import Struct - from .delayed_types import AnyDelayedArray + # Circular import + Array = _imports.Array + Cell = _imports.Cell + MatlabClass = _imports.MatlabClass + MatlabFunction = _imports.MatlabFunction + SparseArray = _imports.SparseArray + Struct = _imports.Struct + AnyDelayedArray = _imports.AnyDelayedArray # Conversion rules: # - we do not convert to matlab's own array types @@ -34,7 +42,7 @@ def from_any(cls, other, **kwargs): # the matlab runtime; # - instead, we convert to python types that mimic matlab types. _from_any = partial(cls.from_any, **kwargs) - _from_runtime = kwargs.pop("_from_runtime", False) + _runtime = kwargs.pop("_runtime", None) if isinstance(other, MatlabType): if isinstance(other, AnyDelayedArray): @@ -56,21 +64,21 @@ def from_any(cls, other, **kwargs): elif type__ == "structarray": # MPython returns a list of dictionaries in data__ # and the array shape in size__. - return Struct._from_runtime(other) + return Struct._from_runtime(other, _runtime) elif type__ == "cell": # MPython returns a list of dictionaries in data__ # and the array shape in size__. - return Cell._from_runtime(other) + return Cell._from_runtime(other, _runtime) elif type__ == "object": # MPython returns the object's fields serialized # in a dictionary. - return MatlabClass._from_runtime(other) + return MatlabClass._from_runtime(other, _runtime) elif type__ == "sparse": # MPython returns the coordinates and values in a dict. - return SparseArray._from_runtime(other) + return SparseArray._from_runtime(other, _runtime) elif type__ == "char": # Character array that is not a row vector @@ -82,26 +90,28 @@ def from_any(cls, other, **kwargs): size = size[:-1] + [1] other["type__"] = "cell" other["size__"] = np.asarray([size]) - return Cell._from_runtime(other) + return Cell._from_runtime(other, _runtime) else: raise ValueError("Don't know what to do with type", type__) else: - other = type(other)(zip(other.keys(), map(_from_any, other.values()))) + other = type(other)( + zip(other.keys(), map(_from_any, other.values())) + ) return Struct.from_any(other) if isinstance(other, (list, tuple, set)): # nested tuples are cells of cells, not cell arrays - if _from_runtime: - return Cell._from_runtime(other) + if _runtime: + return Cell._from_runtime(other, _runtime) else: return Cell.from_any(other) if isinstance(other, (np.ndarray, int, float, complex, bool)): # [array of] numbers -> Array - if _from_runtime: - return Array._from_runtime(other) + if _runtime: + return Array._from_runtime(other, _runtime) else: return Array.from_any(other) @@ -117,20 +127,20 @@ def from_any(cls, other, **kwargs): matlab = _import_matlab() if matlab and isinstance(other, matlab.object): - return MatlabFunction.from_any(other) + return MatlabFunction._from_runtime(other, _runtime) if type(other) in _matlab_array_types(): - return Array._from_runtime(other) + return Array._from_runtime(other, _runtime) if hasattr(other, "__iter__"): # Iterable -> let's try to make it a cell - return cls.from_any(list(other), _from_runtime=_from_runtime) + return cls.from_any(list(other), _runtime=_runtime) raise TypeError(f"Cannot convert {type(other)} into a matlab object.") @classmethod - def _from_runtime(cls, obj): - return cls.from_any(obj, _from_runtime=True) + def _from_runtime(cls, obj, _runtime): + return cls.from_any(obj, _runtime=_runtime) @classmethod def _to_runtime(cls, obj): @@ -162,8 +172,7 @@ def _to_runtime(cls, obj): return obj elif sparse and isinstance(obj, sparse.sparray): - from .SparseArray import SparseArray - + SparseArray = _imports.SparseArray return SparseArray.from_any(obj)._as_runtime() else: @@ -192,14 +201,20 @@ class AnyMatlabArray(MatlabType): @property def as_num(self): - raise TypeError(f"Cannot interpret a {type(self).__name__} as a numeric array") + raise TypeError( + f"Cannot interpret a {type(self).__name__} as a numeric array" + ) @property def as_cell(self): - raise TypeError(f"Cannot interpret a {type(self).__name__} as a cell") + raise TypeError( + f"Cannot interpret a {type(self).__name__} as a cell" + ) @property def as_struct(self): - raise TypeError(f"Cannot interpret a {type(self).__name__} as a struct") + raise TypeError( + f"Cannot interpret a {type(self).__name__} as a struct" + ) # TODO: `as_obj` for object arrays? diff --git a/mpython/core/delayed_types.py b/mpython/core/delayed_types.py index 8318b84..40f2132 100644 --- a/mpython/core/delayed_types.py +++ b/mpython/core/delayed_types.py @@ -1,10 +1,17 @@ import numpy as np from ..exceptions import IndexOrKeyOrAttributeError -from ..utils import _empty_array +from ..utils import _empty_array, DelayedImport from .base_types import AnyMatlabArray +class _imports(DelayedImport): + Array = 'mpython.array.Array' + Cell = 'mpython.cell.Cell' + MatlabClass = 'mpython.matlab_class.MatlabClass' + Struct = 'mpython.struct.Struct' + + class AnyDelayedArray(AnyMatlabArray): """ This is an object that we return when we don't know how an indexed @@ -167,7 +174,9 @@ def as_cell(self) -> "DelayedCell": if self._future is None: self._future = DelayedCell((), self._parent, *self._index) if not isinstance(self._future, DelayedCell): - raise TypeError(f"{type(self._future)} cannot be interpreted as a Cell") + raise TypeError( + f"{type(self._future)} cannot be interpreted as a Cell" + ) return self._future @property @@ -175,7 +184,9 @@ def as_struct(self) -> "DelayedStruct": if self._future is None: self._future = DelayedStruct((), self._parent, *self._index) if not isinstance(self._future, DelayedStruct): - raise TypeError(f"{type(self._future)} cannot be interpreted as a Struct") + raise TypeError( + f"{type(self._future)} cannot be interpreted as a Struct" + ) return self._future @property @@ -183,13 +194,17 @@ def as_num(self) -> "DelayedArray": if self._future is None: self._future = DelayedArray([0], self._parent, *self._index) if not isinstance(self._future, DelayedArray): - raise TypeError(f"{type(self._future)} cannot be interpreted as a Array") + raise TypeError( + f"{type(self._future)} cannot be interpreted as a Array" + ) return self._future def as_obj(self, obj): - from ..matlab_class import MatlabClass - - if self._future is not None and not isinstance(self._future, MatlabClass): + MatlabClass = _imports.MatlabClass + if ( + self._future is not None and + not isinstance(self._future, MatlabClass) + ): raise TypeError( f"{type(self._future)} cannot be interpreted as a {type(obj)}" ) @@ -208,10 +223,10 @@ def __getattr__(self, key): return self.as_struct[key] def __setitem__(self, index, value): - from ..array import Array - from ..cell import Cell - from ..matlab_class import MatlabClass - from ..struct import Struct + Array = _imports.Array + Cell = _imports.Cell + MatlabClass = _imports.MatlabClass + Struct = _imports.Struct if isinstance(index, str): arr = self.as_struct @@ -219,7 +234,8 @@ def __setitem__(self, index, value): elif isinstance(value, MatlabClass): if index not in (0, -1): raise NotImplementedError( - "Implicit advanced indexing not implemented for", type(value) + "Implicit advanced indexing not implemented for", + type(value) ) self.as_obj(value) return self._finalize() @@ -307,8 +323,7 @@ def __init__(self, shape, parent, *index): *index : int | str Index of the future object in its parent. """ - from ..struct import Struct - + Struct = _imports.Struct future = Struct.from_shape(shape) future._delayed_wrapper = self super().__init__(future, parent, *index) @@ -332,8 +347,7 @@ def __init__(self, shape, parent, *index): *index : int | str Index of the future object in its parent. """ - from ..cell import Cell - + Cell = _imports.Cell future = Cell.from_shape(shape) future._delayed_wrapper = self super().__init__(future, parent, *index) @@ -367,8 +381,7 @@ def __init__(self, shape, parent, *index): *index : int | str Index of the future object in its parent. """ - from ..array import Array - + Array = _imports.Array future = Array.from_shape(shape) future._delayed_wrapper = self super().__init__(future, parent, *index) diff --git a/mpython/core/mixin_types.py b/mpython/core/mixin_types.py index 4e89edf..b19775a 100644 --- a/mpython/core/mixin_types.py +++ b/mpython/core/mixin_types.py @@ -8,12 +8,17 @@ import numpy as np -from ..utils import _empty_array, _matlab_array_types +from ..utils import _empty_array, _matlab_array_types, DelayedImport from .base_types import MatlabType from .delayed_types import AnyDelayedArray from .wrapped_types import WrappedArray +class _imports(DelayedImport): + Cell = 'mpython.cell.Cell' + Struct = 'mpython.struct.Struct' + + class _ListishMixin: """These methods are implemented in Cell and Array, but not Struct.""" @@ -79,7 +84,7 @@ def _as_runtime(self) -> dict: ) @classmethod - def _from_runtime(cls, dictobj: dict): + def _from_runtime(cls, dictobj: dict, runtime=None): # NOTE: If there is a single nonzero value, it is passed as a # scalar float, rather than a matlab.double. if dictobj["type__"] != "sparse": @@ -181,7 +186,7 @@ def __imul__(self, value): new_shape[0] *= value np.ndarray.resize(self, new_shape, refcheck=False) for i in range(1, value): - self[i * length : (i + 1) * length] = self[:length] + self[i * length:(i + 1) * length] = self[:length] return self # In lists, __contains__ should be treated as meaning "contains this @@ -272,7 +277,7 @@ def insert(self, index, obj): new_shape = list(np.shape(self)) new_shape[0] += 1 np.ndarray.resize(self, new_shape, refcheck=False) - self[index + 1 :] = self[index:-1] + self[index + 1:] = self[index:-1] self[index] = obj def pop(self, index=-1): @@ -461,10 +466,9 @@ def __getitem__(self, key): # the delayed struct (`delayed`). # # We do not need to use a `DelayedStruct` here. - parent = getattr(self, "_delayed_wrapper", self) - from ..struct import Struct # FIXME: circular imports + Struct = _imports.Struct delayed = Struct(self.shape) opt = dict( @@ -494,7 +498,8 @@ def __setitem__(self, key, value): # in the "deal" array. value = value.broadcast_to_struct(self) opt = dict( - flags=["refs_ok", "zerosize_ok", "multi_index"], op_flags=["readonly"] + flags=["refs_ok", "zerosize_ok", "multi_index"], + op_flags=["readonly"] ) with np.nditer(arr, **opt) as iter: for elem in iter: @@ -544,7 +549,7 @@ def setdefault(self, key, value=None): item.setdefault(key, value) def update(self, other): - from ..struct import Struct # FIXME: circular imports + Struct = _imports.Struct other = Struct.from_any(other) other = np.ndarray.view(other, np.ndarray) @@ -552,7 +557,8 @@ def update(self, other): arr = np.ndarray.view(self, np.ndarray) opt = dict( - flags=["refs_ok", "zerosize_ok", "multi_index"], op_flags=["readonly"] + flags=["refs_ok", "zerosize_ok", "multi_index"], + op_flags=["readonly"] ) with np.nditer(arr, **opt) as iter: for elem in iter: @@ -589,10 +595,9 @@ def __new__(cls, arg, **kwargs): return cls.from_any(arg, **kwargs) def broadcast_to_struct(self, struct): - shape = struct.shape + self.shape[len(struct.shape) :] + shape = struct.shape + self.shape[len(struct.shape):] return np.broadcast_to(self, shape) def to_cell(self): - from ..cell import Cell # FIXME: circular imports - + Cell = _imports.Cell return np.ndarray.view(self, Cell) diff --git a/mpython/core/wrapped_types.py b/mpython/core/wrapped_types.py index 1c9d818..e0f51ab 100644 --- a/mpython/core/wrapped_types.py +++ b/mpython/core/wrapped_types.py @@ -2,6 +2,12 @@ from .base_types import AnyMatlabArray, MatlabType from .delayed_types import AnyDelayedArray, DelayedCell, DelayedStruct +from ..utils import DelayedImport + + +class _imports(DelayedImport): + Cell = 'mpython.cell.Cell' + Struct = 'mpython.struct.Struct' # ---------------------------------------------------------------------- @@ -52,14 +58,16 @@ def _parse_args(cls, *args, **kwargs): if args and __has_dtype: if "dtype" in kwargs: raise TypeError( - f"{cls.__name__}() got multiple values for argument 'dtype'" + f"{cls.__name__}() got multiple values for argument " + f"'dtype'" ) kwargs["dtype"] = args.pop(0) # 2. order {"C", "F"} if args and __has_order: if "order" in kwargs: raise TypeError( - f"{cls.__name__}() got multiple values for argument 'order'" + f"{cls.__name__}() got multiple values for argument " + f"'order'" ) kwargs["order"] = args.pop(0) # 3. no other positionals allowed -> raise @@ -106,7 +114,8 @@ def __repr__(self): # close to np.array_repr, but hides dtype. pre = type(self).__name__ + "(" suf = ")" - return pre + np.array2string(self, prefix=pre, suffix=suf, separator=", ") + suf + arr = np.array2string(self, prefix=pre, suffix=suf, separator=", ") + return pre + arr + suf def __bool__(self): # NumPy arrays do not lower to True/False in a boolean context. @@ -145,7 +154,9 @@ def __setitem__(self, index, value): def __delitem__(self, index): if isinstance(index, tuple): - raise TypeError("Multidimensional indices are not supported in `del`.") + raise TypeError( + "Multidimensional indices are not supported in `del`." + ) # --- list: delete sequentially, from tail to head ------------- if hasattr(index, "__iter__"): @@ -204,7 +215,7 @@ def __delitem__(self, index): index = len(self) + index new_shape = list(np.shape(self)) new_shape[0] -= 1 - self[index:-1] = self[index + 1 :] + self[index:-1] = self[index + 1:] np.ndarray.resize(self, new_shape, refcheck=False) def _resize_for_index(self, index, set_default=True): @@ -282,8 +293,8 @@ def _resize_for_index(self, index, set_default=True): arr[scalar_index] = scalar def _return_delayed(self, index): - from ..cell import Cell - from ..struct import Struct # FIXME: avoid circular import + Cell = _imports.Cell + Struct = _imports.Struct if not isinstance(index, tuple): index = (index,) diff --git a/mpython/matlab_class.py b/mpython/matlab_class.py index 5e5f98b..f7f05ca 100644 --- a/mpython/matlab_class.py +++ b/mpython/matlab_class.py @@ -6,14 +6,30 @@ class MatlabClass(MatlabType): + """ + Base class for wrapped MATLAB classes. + + The MATLAB package wrapped by mpython must define its own inheriting + class that points to an appropriate runtime. + + Example + ------- + ```python + class MyPackageRuntimeMixin: + @classmethod + def _runtime(cls): + return MyPackageRuntime + + class MyPackageClass(MyPackageRuntimeMixin, MatlabClass): + ... + ``` + """ _subclasses = dict() def __new__(cls, *args, _objdict=None, **kwargs): if _objdict is None: if cls.__name__ in MatlabClass._subclasses.keys(): - from .runtime import Runtime - - obj = Runtime.call(cls.__name__, *args, **kwargs) + obj = cls._runtime().call(cls.__name__, *args, **kwargs) else: obj = super().__new__(cls) else: @@ -41,7 +57,7 @@ def from_any(cls, other): return other @classmethod - def _from_runtime(cls, objdict): + def _from_runtime(cls, objdict, runtime=None): if objdict["class__"] in MatlabClass._subclasses.keys(): obj = MatlabClass._subclasses[objdict["class__"]](_objdict=objdict) else: @@ -91,18 +107,17 @@ def _process_index(self, ind, k=1, n=1): # FIXME: This should not need to call matlab try: return tuple( - self._process_index(i, k + 1, len(ind)) for k, i in enumerate(ind) + self._process_index(i, k + 1, len(ind)) + for k, i in enumerate(ind) ) except TypeError: pass - from .runtime import Runtime - if not hasattr(self, "__endfn"): - self.__endfn = Runtime.call("str2func", "end") + self.__endfn = self._runtime().call("str2func", "end") def end(): - return Runtime.call(self.__endfn, self._as_runtime(), k, n) + return self._runtime().call(self.__endfn, self._as_runtime(), k, n) if isinstance(ind, int): if ind >= 0: diff --git a/mpython/matlab_function.py b/mpython/matlab_function.py index 6dfff3a..f836d67 100644 --- a/mpython/matlab_function.py +++ b/mpython/matlab_function.py @@ -6,6 +6,8 @@ class MatlabFunction(MatlabType): """ Wrapper for matlab function handles. + End users should not have to instantiate such objects themselves. + Example ------- ```python @@ -14,7 +16,7 @@ class MatlabFunction(MatlabType): ``` """ - def __init__(self, matlab_object): + def __init__(self, matlab_object, runtime): super().__init__() matlab = _import_matlab() @@ -22,21 +24,20 @@ def __init__(self, matlab_object): raise TypeError("Expected a matlab.object") self._matlab_object = matlab_object + self._runtime = runtime def _as_runtime(self): return self._matlab_object @classmethod - def _from_runtime(cls, other): - return cls(other) + def _from_runtime(cls, other, runtime): + return cls(other, runtime) @classmethod - def from_any(cls, other): + def from_any(cls, other, runtime=None): if isinstance(other, MatlabFunction): return other - return cls._from_runtime(other) + return cls._from_runtime(other, runtime) def __call__(self, *args, **kwargs): - from .runtime import Runtime - - return Runtime.call(self._matlab_object, *args, **kwargs) + return self._runtime.call(self._matlab_object, *args, **kwargs) diff --git a/mpython/runtime.py b/mpython/runtime.py index 302bc81..9e6d0c1 100644 --- a/mpython/runtime.py +++ b/mpython/runtime.py @@ -1,72 +1,81 @@ +from abc import ABC, abstractmethod + from .core import MatlabType from .utils import _import_matlab -class Runtime: - """Namespace that holds the matlab runtime. All methods are static.""" +class Runtime(ABC): + """Namespace that holds the matlab runtime. + + Wrapped packages should implement their own inheriting class + and define the `_import` method. + + Example + ------- + ```python + class SPMRuntime(Runtime): + + @classmethod + def _import_runtime(cls): + import spm_runtime + return spm_runtime + ``` + """ - _initialize = None _instance = None verbose = True - @staticmethod - def instance(): - if Runtime._instance is None: - if Runtime.verbose: + @classmethod + @abstractmethod + def _import_runtime(cls): + """""" + ... + + @classmethod + def instance(cls): + if cls._instance is None: + if cls.verbose: print("Initializing Matlab Runtime...") - Runtime._import_initialize() - Runtime._instance = Runtime._initialize() - return Runtime._instance + cls._init_instance() + return cls._instance - @staticmethod - def call(fn, *args, **kwargs): - (args, kwargs) = Runtime._process_argin(*args, **kwargs) - res = Runtime.instance().mpython_endpoint(fn, *args, **kwargs) - return Runtime._process_argout(res) + @classmethod + def call(cls, fn, *args, **kwargs): + (args, kwargs) = cls._process_argin(*args, **kwargs) + res = cls.instance().mpython_endpoint(fn, *args, **kwargs) + return cls._process_argout(res) - @staticmethod - def _process_argin(*args, **kwargs): + @classmethod + def _process_argin(cls, *args, **kwargs): to_runtime = MatlabType._to_runtime args = tuple(map(to_runtime, args)) kwargs = dict(zip(kwargs.keys(), map(to_runtime, kwargs.values()))) return args, kwargs - @staticmethod - def _process_argout(res): - return MatlabType._from_runtime(res) + @classmethod + def _process_argout(cls, res): + return MatlabType._from_runtime(res, cls) - @staticmethod - def _import_initialize(): + @classmethod + def _init_instance(cls): # NOTE(YB) # I moved the import within a function so that array wrappers # can be imported and used even when matlab is not properly setup. - if Runtime._initialize: + if cls._instance: return try: - from spm._spm import initialize - - Runtime._initialize = initialize + cls._instance = cls._import_runtime() + # Make sure matlab is imported + _import_matlab() except ImportError as e: - # ~~~ UNUSED ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # import os - # installer_path = os.path.join( - # os.path.dirname(os.path.abspath(__file__)), - # '_spm', - # 'resources', - # 'RuntimeInstaller.install' - # ) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - print(Runtime._help) + print(cls._help) raise e - # Make sure matlab is imported - _import_matlab() - _help = """ - Failed to import spm._spm. This can be due to a failure to find Matlab - Runtime. Please verify that Matlab Runtime is installed and its path is set. - See https://www.mathworks.com/help/compiler/mcr-path-settings-for-run-time-deployment.html - for instructions on how to setup the path. + Failed to import package runtime. This can be due to a failure to find the + MATLAB Runtime. Please verify that MATLAB Runtime is installed and can be + discovered. See https://github.com/balbasty/matlab-runtime for instructions + on how to install the MATLAB Runtime. If the issue persists, please open an issue with the entire error - message at https://github.com/spm/spm-python/issues. + message at https://github.com/MPython-Package-Factory/mpython-core/issues. """ diff --git a/mpython/struct.py b/mpython/struct.py index de026c1..ec0cae0 100644 --- a/mpython/struct.py +++ b/mpython/struct.py @@ -1,7 +1,11 @@ import numpy as np from .core import DelayedStruct, MatlabType, WrappedArray, _DictMixin -from .utils import _copy_if_needed, _empty_array +from .utils import _copy_if_needed, _empty_array, DelayedImport + + +class _imports(DelayedImport): + Cell = 'mpython.cell.Cell' class Struct(_DictMixin, WrappedArray): @@ -83,7 +87,8 @@ def _DEFAULT(self, shape: list = ()) -> np.ndarray: data = np.empty(shape, dtype=dict) opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["writeonly", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["writeonly", "no_broadcast"] ) with np.nditer(data, **opt) as iter: for elem in iter: @@ -125,7 +130,7 @@ def _as_runtime(self) -> dict: return dict(type__="structarray", size__=size, data__=data) @classmethod - def _from_runtime(cls, objdict: dict) -> "Struct": + def _from_runtime(cls, objdict: dict, runtime=None) -> "Struct": if objdict["type__"] != "structarray": raise TypeError("objdict is not a structarray") size = np.array(objdict["size__"], dtype=np.uint64).ravel() @@ -140,18 +145,20 @@ def _from_runtime(cls, objdict: dict) -> "Struct": obj = data.view(cls) except Exception: raise RuntimeError( - f"Failed to construct Struct data:\n data={data}\n objdict={objdict}" + f"Failed to construct Struct data:\n" + f" data={data}\n objdict={objdict}" ) # recurse opt = dict( - flags=["refs_ok", "zerosize_ok"], op_flags=["readonly", "no_broadcast"] + flags=["refs_ok", "zerosize_ok"], + op_flags=["readonly", "no_broadcast"] ) with np.nditer(data, **opt) as iter: for elem in iter: item = elem.item() for key, val in item.items(): - item[key] = MatlabType._from_runtime(val) + item[key] = MatlabType._from_runtime(val, runtime) return obj @@ -264,8 +271,7 @@ def from_any(cls, other, **kwargs) -> "Struct": @classmethod def from_cell(cls, other, **kwargs) -> "Struct": """See `from_any`.""" - from .cell import Cell - + Cell = _imports.Cell if not isinstance(other, Cell): raise TypeError(f"Expected a {Cell} but got a {type(other)}.") return cls.from_any(other, **kwargs) @@ -346,12 +352,10 @@ def as_dict(self, keys=None) -> dict: for key in keys: asdict[key].append(item[key]) - from .cell import Cell - + Cell = _imports.Cell for key in keys: asdict[key] = Cell.from_any(asdict[key]) - raise ValueError(keys) return asdict def _allkeys(self): diff --git a/mpython/utils.py b/mpython/utils.py index 8dd47f9..ebd0370 100644 --- a/mpython/utils.py +++ b/mpython/utils.py @@ -1,3 +1,5 @@ +import importlib + import numpy as np # If scipy is available, convert matlab sparse matrices scipy.sparse @@ -12,8 +14,74 @@ # ---------------------------------------------------------------------- -# We'll complain later if the runtime is not instantiated +class DelayedImportElement: + + def __init__(self, name, import_path=None): + self.name = name + self.import_path = import_path + + def _import(self): + assert self.import_path + import_path = self.import_path + try: + var = importlib.import_module(import_path) + except ModuleNotFoundError as e: + try: + *import_path, var = import_path.split('.') + import_path = '.'.join(import_path) + mod = importlib.import_module(import_path) + var = getattr(mod, var) + except (ModuleNotFoundError, AttributeError): + raise e + return var + + def __get__(self, instance, owner): + assert instance is None + imported = self._import() + setattr(owner, self.name, imported) + return imported + + +class DelayedImport: + """A utility to delay the import of modules or variables. + + Until they are imported, import paths are wrapped in a + `DelayedImportElement` object. The first time an element is accessed, + it triggers the underlying import and assign the imported module or + object into the `DelayedImport` child class, while getting rid + of the `DelayedImportElement` wrapper. Thereby, the next time the + element is accessed, the module is directly obtained. This strategy + minimizes overhead on subsequent calls (no need to test whether + the module has already been imported or not). + + Example + ------- + ```python + # module_with_definitions.py + class _imports(DelayedImport): + Array = 'mpython.array.Array' + Cell = 'mpython.cell.Cell' + + def foo(): + Array = _imports.Array + Cell = _imports.Cell + ``` + """ + def __init_subclass__(cls): + for key, val in cls.__dict__.items(): + if key.startswith("__"): + continue + setattr(cls, key, DelayedImportElement(key, val)) + + def _import_matlab(): + """ + Delayed matlab import. + + This allows to only complain about the lack of a runtime if we + really use the runtime. Note that most of the MPython types do + not need the runtime. + """ try: import matlab except (ImportError, ModuleNotFoundError):