Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactors the MatlabWrapper to load Matlab in the background #41

Merged
merged 3 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,4 @@ jobs:
- name: Install and Test with pytest
run: |
python -m pip install -e .[Dev]
pytest tests/ --cov=${{ github.event.repository.name }} --cov-report=html:htmlcov --cov-report=term
pytest tests/ --cov=RATpy --cov-report=term
33 changes: 24 additions & 9 deletions RATpy/wrappers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pathlib
from contextlib import suppress
from typing import Callable, Tuple

import numpy as np
Expand All @@ -7,6 +8,24 @@
import RATpy.rat_core


def start_matlab():
"""Starts MATLAB asynchronously and returns a future to retrieve the engine later

Returns
-------
future : matlab.engine.futureresult.FutureResult
A future used to get the actual matlab engine

"""
future = None
with suppress(ImportError):
import matlab.engine

future = matlab.engine.start_matlab(background=True)

return future


class MatlabWrapper:
"""Creates a python callback for a MATLAB function.

Expand All @@ -17,21 +36,17 @@ class MatlabWrapper:

"""

loader = start_matlab()

def __init__(self, filename: str) -> None:
self.engine = None
try:
import matlab.engine
except ImportError:
if self.loader is None:
raise ImportError("matlabengine is required to use MatlabWrapper") from None
self.engine = matlab.engine.start_matlab()

self.engine = self.loader.result()
path = pathlib.Path(filename)
self.engine.cd(str(path.parent), nargout=0)
self.function_name = path.stem

def __del__(self):
if self.engine is not None:
self.engine.quit()

def getHandle(self) -> Callable[[ArrayLike, ArrayLike, ArrayLike, int, int], Tuple[ArrayLike, float]]:
"""Returns a wrapper for the custom MATLAB function

Expand Down
1 change: 0 additions & 1 deletion requirements-dev.txt

This file was deleted.

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pytest >= 7.4.0
pytest-cov >= 4.1.0
matplotlib >= 3.8.3
StrEnum >= 0.4.15; python_version < '3.11'
ruff >= 0.4.10
25 changes: 13 additions & 12 deletions tests/test_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,13 +618,14 @@ def test_make_input(test_project, test_problem, test_cells, test_limits, test_pr
"domainRatio",
]

mocked_matlab_module = mock.MagicMock()
mocked_matlab_future = mock.MagicMock()
mocked_engine = mock.MagicMock()
mocked_matlab_module.engine.start_matlab.return_value = mocked_engine
mocked_matlab_future.result.return_value = mocked_engine

with mock.patch.dict(
"sys.modules",
{"matlab": mocked_matlab_module, "matlab.engine": mocked_matlab_module.engine},
with mock.patch.object(
RATpy.wrappers.MatlabWrapper,
"loader",
mocked_matlab_future,
), mock.patch.object(RATpy.rat_core, "DylibEngine", mock.MagicMock()), mock.patch.object(
RATpy.inputs,
"get_python_handle",
Expand Down Expand Up @@ -757,13 +758,13 @@ def test_make_cells(test_project, test_cells, request) -> None:
test_project = request.getfixturevalue(test_project)
test_cells = request.getfixturevalue(test_cells)

mocked_matlab_module = mock.MagicMock()
mocked_matlab_engine = mock.MagicMock()
mocked_matlab_module.engine.start_matlab.return_value = mocked_matlab_engine

with mock.patch.dict(
"sys.modules",
{"matlab": mocked_matlab_module, "matlab.engine": mocked_matlab_module.engine},
mocked_matlab_future = mock.MagicMock()
mocked_engine = mock.MagicMock()
mocked_matlab_future.result.return_value = mocked_engine
with mock.patch.object(
RATpy.wrappers.MatlabWrapper,
"loader",
mocked_matlab_future,
), mock.patch.object(RATpy.rat_core, "DylibEngine", mock.MagicMock()), mock.patch.object(
RATpy.inputs,
"get_python_handle",
Expand Down
13 changes: 7 additions & 6 deletions tests/test_wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@


def test_matlab_wrapper() -> None:
with mock.patch.dict("sys.modules", {"matlab": mock.MagicMock(side_effect=ImportError)}), pytest.raises(
ImportError,
with (
mock.patch.object(RATpy.wrappers.MatlabWrapper, "loader", None),
pytest.raises(ImportError),
):
RATpy.wrappers.MatlabWrapper("demo.m")
mocked_matlab_module = mock.MagicMock()
mocked_engine = mock.MagicMock()
mocked_matlab_module.engine.start_matlab.return_value = mocked_engine

with mock.patch.dict("sys.modules", {"matlab": mocked_matlab_module, "matlab.engine": mocked_matlab_module.engine}):
mocked_matlab_future = mock.MagicMock()
mocked_engine = mock.MagicMock()
mocked_matlab_future.result.return_value = mocked_engine
with mock.patch.object(RATpy.wrappers.MatlabWrapper, "loader", mocked_matlab_future):
wrapper = RATpy.wrappers.MatlabWrapper("demo.m")
assert wrapper.function_name == "demo"
mocked_engine.cd.assert_called_once()
Expand Down
Loading