Skip to content

Commit

Permalink
RELEASE v0.1.6 (#22)
Browse files Browse the repository at this point in the history
* Update Matlab search

Add search on system PATH
Add mapping between Release and version numbers

* Change to use evalAsync

* Add type() method to Matlab as  is reserved in Python

* Add way to modify plain struct submember properties of Matlab objects

* Enable live stdout redirection to Python

* CI bugfixes

Update cibuildwheel version.
Workaround test segfault.
Fix syntax error in utils.py
Update actions to node-20
Change to setup-micromamba

* Update CITATION.cff and CHANGELOG.md to v0.1.6
  • Loading branch information
mducle authored Apr 26, 2024
1 parent f34229f commit 3db5507
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 49 deletions.
23 changes: 12 additions & 11 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,20 @@ jobs:
os: [ubuntu-22.04, windows-2022, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache MCR
if: ${{ matrix.os != 'ubuntu-22.04' }}
id: cache-mcr
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: mcr
key: ${{ runner.os }}-matlab-${{ env.MCRVER }}-mcr
lookup-only: true
- name: Cache gists
id: cache-gists
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: gists
key: ${{ runner.os }}-gists
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
os: [ubuntu-22.04, windows-2022, macos-12]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup variables
Expand Down Expand Up @@ -143,28 +143,27 @@ jobs:
uses: matlab-actions/[email protected]
with:
release: ${{ env.MLVER }}
- uses: actions/cache/restore@v3
- uses: actions/cache/restore@v4
if: ${{ matrix.os != 'ubuntu-22.04' }}
with:
path: mcr
key: ${{ runner.os }}-matlab-${{ env.MCRVER }}-mcr
fail-on-cache-miss: true
- uses: actions/cache/restore@v3
- uses: actions/cache/restore@v4
with:
path: gists
key: ${{ runner.os }}-gists
fail-on-cache-miss: true
- uses: pypa/cibuildwheel@v2.12.0
- uses: pypa/cibuildwheel@v2.17.0
env:
CIBW_BUILD: ${{ env.CIBW }}
CIBW_ENVIRONMENT: >-
MATLABEXECUTABLE="${{ env.MLPREFIX }}${{ env.MATLABEXECUTABLE }}"
HOSTDIRECTORY="${{ env.MLPREFIX }}${{ env.HOSTDIRECTORY }}"
CIBW_BUILD_VERBOSITY: 1
MACOSX_DEPLOYMENT_TARGET: "10.15"
- uses: mamba-org/provision-with-micromamba@main
- uses: mamba-org/setup-micromamba@v1
with:
environment-file: false
cache-downloads: true
- name: Install wheels and run test
run: |
Expand All @@ -176,7 +175,9 @@ jobs:
eval "$($MAMBA_EXE shell activate py$pyver)"
python -m pip install wheelhouse/*cp$(echo $pyver | sed s/\\.//)*
cd test
python run_test.py
# Sometimes the test results in a segfault on exit
python run_test.py || true
test -f success
cd ..
done
- name: Upload release wheels
Expand All @@ -187,7 +188,7 @@ jobs:
#- name: Setup tmate
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}_artifacts.zip
path: |
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# [v0.1.6](https://github.com/pace-neutrons/libpymcr/compare/v0.1.5...v0.1.6)

## Bugfixes

Bugfixes for various user reported issues when used with pace-python and PySpinW.

* Add search of `matlab` executable on path for Linux.
* Add a `type` method to interrogate Matlab type (fixes issue with [pace-python-demo example](https://github.com/pace-neutrons/pace-python-demo/blob/main/demo.py#L86))
* Add a proxy to allow plain struct properties of a class to be manipulated like in Matlab (so `class.prop.subprop = 1` will work if `class.prop` is a plain Matlab `struct`).
* Change to using `evalAsync` in Matlab and as part of this polls the output streams every 1ms and prints output to Python - this allows synchronous output to both console, Jupyter and Spyder without additional code.


# [v0.1.5](https://github.com/pace-neutrons/libpymcr/compare/v0.1.4...v0.1.5)

## Bugfixes for PySpinW
Expand Down
4 changes: 2 additions & 2 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ authors:
given-names: "Gregory S."
orcid: https://orcid.org/0000-0002-2787-8054
title: "libpymcr"
version: "0.1.5"
date-released: "2023-03-24"
version: "0.1.6"
date-released: "2024-04-26"
license: "GPL-3.0-only"
repository: "https://github.com/pace-neutrons/libpymcr"
url: "https://github.com/pace-neutrons/libpymcr"
Expand Down
6 changes: 6 additions & 0 deletions libpymcr/Matlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ def __getattr__(self, name):
"""
return NamespaceWrapper(self._interface, name)

def type(self, obj):
if hasattr(obj, 'handle'):
return self._interface.call('class', obj.handle, nargout=1)
else:
return str(type(obj))

def get_matlab_functions(self):
"""
Returns a list of public functions in this CTF archive
Expand Down
31 changes: 30 additions & 1 deletion libpymcr/MatlabProxyObject.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,32 @@ def unwrap(inputs, interface):
else:
return inputs


class DictPropertyWrapper:
# A proxy for dictionary properties of classes to allow Matlab .dot syntax
def __init__(self, val, name, parent):
assert isinstance(val, dict), "DictPropertyWrapper can only wrap dict objects"
self.__dict__['val'] = val
self.__dict__['name'] = name
self.__dict__['parent'] = parent

def __getattr__(self, name):
rv = self.val[name]
if isinstance(rv, dict):
rv = DictPropertyWrapper(rv, name, self)
return rv

def __setattr__(self, name, value):
self.val[name] = value
setattr(self.parent, self.name, self.val)

def __repr__(self):
rv = "Matlab struct with fields:\n"
for k, v in self.val.items():
rv += f" {k}: {v}\n"
return rv


class matlab_method:
def __init__(self, proxy, method):
self.proxy = proxy
Expand Down Expand Up @@ -114,7 +140,10 @@ def __getattr__(self, name):
# if it's a property, just retrieve it
if name in self._getAttributeNames():
try:
return wrap(self.interface.call('subsref', self.handle, {'type':'.', 'subs':name}), self.interface)
rv = wrap(self.interface.call('subsref', self.handle, {'type':'.', 'subs':name}), self.interface)
if isinstance(rv, dict):
rv = DictPropertyWrapper(rv, name, self)
return rv
except TypeError:
return None
# if it's a method, wrap it in a functor
Expand Down
38 changes: 30 additions & 8 deletions libpymcr/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import inspect
import dis
import linecache
import shutil
from pathlib import Path


Expand All @@ -22,6 +23,8 @@
"CALL_FUNCTION_EX", "LOAD_METHOD", "CALL_METHOD", "DICT_MERGE", "DICT_UPDATE", "LIST_EXTEND",
}

MLVERDIC = {f'R{rv[0]}{rv[1]}':f'9.{vr}' for rv, vr in zip([[yr, ab] for yr in range(2017,2023) for ab in ['a', 'b']], range(2, 14))}
MLVERDIC.update({'R2023a':'9.14', 'R2023b':'23.2'})

def get_nret_from_dis(frame):
# Tries to get the number of return values for a function
Expand Down Expand Up @@ -164,8 +167,18 @@ def __init__(self, version):
else:
raise RuntimeError(f'Operating system {self.system} is not supported.')

@property
def ver(self):
return self._ver

@ver.setter
def ver(self, val):
self._ver = str(val)
if self._ver.startswith('R') and self._ver in MLVERDIC.keys():
self._ver = MLVERDIC[self._ver]

def find_version(self, root_dir):
print(f'Searching for Matlab in {root_dir}')
print(f'Searching for Matlab {self.ver} in {root_dir}')
def find_file(path, filename, max_depth=3):
""" Finds a file, will return first match"""
for depth in range(max_depth + 1):
Expand Down Expand Up @@ -202,22 +215,29 @@ def guess_path(self, mlPath=[]):
pp = ml_env.split('/')[1:]
ml_env = pp[0] + ':\\' + '\\'.join(pp[1:])
mlPath += [os.path.abspath(os.path.join(ml_env, '..', '..'))]
print(f'mlPath={mlPath}')
for possible_dir in mlPath + GUESSES[self.system]:
if os.path.isdir(possible_dir):
rv = self.find_version(possible_dir)
if rv is not None:
return rv
return None

def guess_from_env(self):
ld_path = os.getenv(self.path_var)
def guess_from_env(self, ld_path=None):
if ld_path is None:
ld_path = os.getenv(self.path_var)
if ld_path is None: return None
for possible_dir in ld_path.split(self.sep):
if os.path.exists(os.path.join(possible_dir, self.file_to_find)):
return os.path.abspath(os.path.join(possible_dir, '..', '..'))
return None

def guess_from_syspath(self):
matlab_exe = shutil.which('matlab')
if matlab_exe is None:
return None if self.system == 'Windows' else self.guess_from_env('PATH')
mlbinpath = os.path.dirname(os.path.realpath(matlab_exe))
return self.find_version(os.path.abspath(os.path.join(mlbinpath, '..')))

def env_not_set(self):
# Determines if the environment variables required by the MCR are set
if self.path_var not in os.environ:
Expand All @@ -242,7 +262,7 @@ def set_environment(self, mlPath=None):
return None


def checkPath(runtime_version, mlPath=None):
def checkPath(runtime_version, mlPath=None, error_if_not_found=True):
"""
Sets the environmental variables for Win, Mac, Linux
Expand All @@ -260,14 +280,16 @@ def checkPath(runtime_version, mlPath=None):
raise FileNotFoundError(f'Input Matlab folder {mlPath} not found')
else:
mlPath = obj.guess_from_env()
if mlPath is None:
mlPath = obj.guess_from_syspath()
if mlPath is None:
mlPath = obj.guess_path()
if mlPath is None:
raise RuntimeError('Cannot find Matlab')
else:
if mlPath is not None:
ld_path = obj.sep.join([os.path.join(mlPath, sub, obj.arch) for sub in obj.required_dirs])
os.environ[obj.path_var] = ld_path
#print('Set ' + os.environ.get(obj.path_var))
elif error_if_not_found:
raise RuntimeError('Cannot find Matlab')
#else:
# print('Found: ' + os.environ.get(obj.path_var))

Expand Down
28 changes: 23 additions & 5 deletions src/libpymcr.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "load_matlab.hpp"
#include "libpymcr.hpp"
#include <chrono>
#include <ratio>
#include <pybind11/stl.h>

namespace libpymcr {
Expand All @@ -24,6 +26,22 @@ namespace libpymcr {
return nargout;
}

template <class T> T matlab_env::evalloop(matlab::cpplib::FutureResult<T> resAsync) {
std::chrono::duration<int, std::milli> period(1);
std::future_status status = resAsync.wait_for(std::chrono::duration<int, std::milli>(1));
while (status != std::future_status::ready) {
status = resAsync.wait_for(period);
// Prints outputs and errors
if(_m_output.get()->in_avail() > 0) {
py::gil_scoped_acquire gil_acquire;
py::print(_m_output.get()->str(), py::arg("flush")=true);
py::gil_scoped_release gil_release;
_m_output.get()->str(std::basic_string<char16_t>());
}
}
return resAsync.get();
}

py::object matlab_env::feval(const std::u16string &funcname, py::args args, py::kwargs& kwargs) {
// Calls Matlab function
const size_t nlhs = 0;
Expand All @@ -38,18 +56,18 @@ namespace libpymcr {
py::gil_scoped_release gil_release;
if (nargout == 1) {
if (m_args.size() == 1) {
outputs.push_back(_lib->feval(funcname, m_args[0], _m_output_buf, _m_error_buf));
outputs.push_back(evalloop(_lib->fevalAsync(funcname, m_args[0], _m_output_buf, _m_error_buf)));
} else {
outputs.push_back(_lib->feval(funcname, m_args, _m_output_buf, _m_error_buf));
outputs.push_back(evalloop(_lib->fevalAsync(funcname, m_args, _m_output_buf, _m_error_buf)));
}
} else {
outputs = _lib->feval(funcname, nargout, m_args, _m_output_buf, _m_error_buf);
outputs = evalloop(_lib->fevalAsync(funcname, nargout, m_args, _m_output_buf, _m_error_buf));
}
// Re-aquire the GIL
py::gil_scoped_acquire gil_acquire;
// Prints outputs and errors
if(_m_output.get()->in_avail() > 0) {
py::print(_m_output.get()->str(), py::arg("flush")=true); }
//if(_m_output.get()->in_avail() > 0) {
// py::print(_m_output.get()->str(), py::arg("flush")=true); }
if(_m_error.get()->in_avail() > 0) {
py::print(_m_error.get()->str(), py::arg("file")=py::module::import("sys").attr("stderr"), py::arg("flush")=true); }
// Converts outputs to Python types
Expand Down
1 change: 1 addition & 0 deletions src/libpymcr.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ namespace libpymcr {
std::shared_ptr<StreamBuffer> _m_error_buf = std::static_pointer_cast<StreamBuffer>(_m_error);
pymat_converter _converter;
size_t _parse_inputs(std::vector<matlab::data::Array>& m_args, py::args py_args, py::kwargs& py_kwargs);
template <class T> T evalloop(matlab::cpplib::FutureResult<T> resAsync);
public:
py::object feval(const std::u16string &funcname, py::args args, py::kwargs& kwargs);
py::object call(py::args args, py::kwargs& kwargs);
Expand Down
Loading

0 comments on commit 3db5507

Please sign in to comment.