From ec0f3e5827427df8452acc7704b8f54aad739f39 Mon Sep 17 00:00:00 2001 From: balbasty Date: Tue, 29 Apr 2025 16:52:56 +0100 Subject: [PATCH 1/7] ENH: do not explicitly import spm._spm + nicer (?) way to handle circular imports --- mpython/array.py | 10 ++-- mpython/cell.py | 34 ++++++------ mpython/core/base_types.py | 79 ++++++++++++++++------------ mpython/core/delayed_types.py | 49 +++++++++++------- mpython/core/mixin_types.py | 29 ++++++----- mpython/core/wrapped_types.py | 25 ++++++--- mpython/matlab_class.py | 33 ++++++++---- mpython/matlab_function.py | 17 +++--- mpython/runtime.py | 97 +++++++++++++++++++---------------- mpython/struct.py | 26 ++++++---- mpython/utils.py | 83 +++++++++++++++++++++++++++++- 11 files changed, 321 insertions(+), 161 deletions(-) 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..e417f53 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.MatlabFunction' 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..c1f36b4 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..3910c45 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, _runtime=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..fffd342 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,87 @@ # ---------------------------------------------------------------------- -# We'll complain later if the runtime is not instantiated +class DelayedImportElement: + + class MarkAsImported: + def __init__(self, obj): + self.obj = obj + + 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): + print("__get__", self.name) + assert instance is None + imported = self.MarkAsImported(self._import()) + setattr(owner, self.name, imported) + return imported + + def __set__(self, instance, value): + print("__set__", self.name) + if isinstance(value, self.MarkAsImported): + delattr(instance, self.name) + setattr(instance, self.name, value.obj) + else: + self.import_path = value + + +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 to + not need the runtime. + """ try: import matlab except (ImportError, ModuleNotFoundError): From 17111fd70b846caefd8ecba6c8f51a812a7ad68b Mon Sep 17 00:00:00 2001 From: balbasty Date: Tue, 29 Apr 2025 17:11:06 +0100 Subject: [PATCH 2/7] FIX: couple of typos --- mpython/core/base_types.py | 2 +- mpython/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mpython/core/base_types.py b/mpython/core/base_types.py index e417f53..4f990d5 100644 --- a/mpython/core/base_types.py +++ b/mpython/core/base_types.py @@ -12,7 +12,7 @@ class _imports(DelayedImport): SparseArray = 'mpython.sparse_array.SparseArray' MatlabClass = 'mpython.matlab_class.MatlabClass' MatlabFunction = 'mpython.matlab_function.MatlabFunction' - AnyDelayedArray = 'mpython.core.delayed_types.MatlabFunction' + AnyDelayedArray = 'mpython.core.delayed_types.AnyDelayedArray' class MatlabType: diff --git a/mpython/utils.py b/mpython/utils.py index fffd342..a120f0b 100644 --- a/mpython/utils.py +++ b/mpython/utils.py @@ -92,7 +92,7 @@ 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 to + really use the runtime. Note that most of the MPython types do not need the runtime. """ try: From 3589200eec78f2f519a7f877ab9ef31b1c480d6a Mon Sep 17 00:00:00 2001 From: balbasty Date: Tue, 13 May 2025 13:42:46 +0100 Subject: [PATCH 3/7] Remove traces --- mpython/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mpython/utils.py b/mpython/utils.py index a120f0b..b8ee566 100644 --- a/mpython/utils.py +++ b/mpython/utils.py @@ -40,14 +40,12 @@ def _import(self): return var def __get__(self, instance, owner): - print("__get__", self.name) assert instance is None imported = self.MarkAsImported(self._import()) setattr(owner, self.name, imported) return imported def __set__(self, instance, value): - print("__set__", self.name) if isinstance(value, self.MarkAsImported): delattr(instance, self.name) setattr(instance, self.name, value.obj) From 47e3a693bcabc5986bb0c300acb24df9e2362dbe Mon Sep 17 00:00:00 2001 From: balbasty Date: Tue, 13 May 2025 13:43:05 +0100 Subject: [PATCH 4/7] [Fix] wrong argument name --- mpython/core/base_types.py | 4 ++-- mpython/runtime.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mpython/core/base_types.py b/mpython/core/base_types.py index 4f990d5..26fd942 100644 --- a/mpython/core/base_types.py +++ b/mpython/core/base_types.py @@ -139,8 +139,8 @@ def from_any(cls, other, **kwargs): raise TypeError(f"Cannot convert {type(other)} into a matlab object.") @classmethod - def _from_runtime(cls, obj, runtime): - return cls.from_any(obj, _runtime=runtime) + def _from_runtime(cls, obj, _runtime): + return cls.from_any(obj, _runtime=_runtime) @classmethod def _to_runtime(cls, obj): diff --git a/mpython/runtime.py b/mpython/runtime.py index 3910c45..9e6d0c1 100644 --- a/mpython/runtime.py +++ b/mpython/runtime.py @@ -54,7 +54,7 @@ def _process_argin(cls, *args, **kwargs): @classmethod def _process_argout(cls, res): - return MatlabType._from_runtime(res, _runtime=cls) + return MatlabType._from_runtime(res, cls) @classmethod def _init_instance(cls): From 0cd4c95c8e1c4400e3ca03a8352d4ce46abc3afe Mon Sep 17 00:00:00 2001 From: balbasty Date: Tue, 20 May 2025 15:08:44 +0100 Subject: [PATCH 5/7] [Fix] fix DelayedImport logic --- mpython/utils.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/mpython/utils.py b/mpython/utils.py index b8ee566..ebd0370 100644 --- a/mpython/utils.py +++ b/mpython/utils.py @@ -16,10 +16,6 @@ class DelayedImportElement: - class MarkAsImported: - def __init__(self, obj): - self.obj = obj - def __init__(self, name, import_path=None): self.name = name self.import_path = import_path @@ -41,17 +37,10 @@ def _import(self): def __get__(self, instance, owner): assert instance is None - imported = self.MarkAsImported(self._import()) + imported = self._import() setattr(owner, self.name, imported) return imported - def __set__(self, instance, value): - if isinstance(value, self.MarkAsImported): - delattr(instance, self.name) - setattr(instance, self.name, value.obj) - else: - self.import_path = value - class DelayedImport: """A utility to delay the import of modules or variables. From da3ec3385c0fba41cf6f6f4b3296c22046006bcd Mon Sep 17 00:00:00 2001 From: balbasty Date: Thu, 19 Jun 2025 16:26:42 +0100 Subject: [PATCH 6/7] [FIX] typo: _Runtime -> _runtime --- mpython/matlab_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mpython/matlab_class.py b/mpython/matlab_class.py index c1f36b4..f7f05ca 100644 --- a/mpython/matlab_class.py +++ b/mpython/matlab_class.py @@ -114,7 +114,7 @@ def _process_index(self, ind, k=1, n=1): pass if not hasattr(self, "__endfn"): - self.__endfn = self._Runtime.call("str2func", "end") + self.__endfn = self._runtime().call("str2func", "end") def end(): return self._runtime().call(self.__endfn, self._as_runtime(), k, n) From 8673465b716875e82e7162a23518bc8368b38d58 Mon Sep 17 00:00:00 2001 From: Johan Medrano Date: Fri, 20 Jun 2025 13:44:20 -0400 Subject: [PATCH 7/7] [Mnt] Update version number before PR merging --- mpython/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"