Skip to content
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
6 changes: 5 additions & 1 deletion pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@
obj = javaObject('org.json.JSONObject');
keys = fieldnames(value);
for i = 1:length(keys)
obj.put(keys{i},dump_data_(value.(keys{i}), options));
try
obj.put(keys{i},dump_data_(value.(keys{i}), options));
catch ME
obj.put(keys{i}, dump_data_(ME.message, options))
end
end
else
error('json:typeError', 'Unsupported data type: %s', class(value));
Expand Down
129 changes: 126 additions & 3 deletions pymatbridge/pymatbridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import subprocess
import sys
import json
import types
import weakref
from uuid import uuid4

from numpy import ndarray, generic, float64, frombuffer, asfortranarray
Expand Down Expand Up @@ -263,22 +265,29 @@ def is_function_processor_working(self):
def _json_response(self, **kwargs):
return json.loads(self._response(**kwargs), object_hook=decode_pymat)

def run_func(self, func_path, func_args=None, nargout=1):
def run_func(self, func_path, *func_args, **kwargs):
"""Run a function in Matlab and return the result.

Parameters
----------
func_path: str
Name of function to run or a path to an m-file.
func_args: object
func_args: object, optional
Function args to send to the function.
nargout: int
nargout: int, optional
Desired number of return arguments.
kwargs:
Keyword arguments are passed to Matlab in the form [key, val] so
that matlab.plot(x, y, '--', LineWidth=2) would be translated into
plot(x, y, '--', 'LineWidth', 2)

Returns
-------
Result dictionary with keys: 'message', 'result', and 'success'
"""
nargout = kwargs.pop('nargout', 1)
func_args += tuple(item for pair in zip(kwargs.keys(), kwargs.values())
for item in pair)
return self._json_response(cmd='run_function',
func_path=func_path,
func_args=func_args,
Expand Down Expand Up @@ -323,6 +332,64 @@ def _set_sparse_variable(self, varname, value):
self.run_code('clear {0}keys {0}values'.format(prefix))
return result

def __getattr__(self, name):
"""If an attribute is not found, try to create a bound method"""
return self._bind_method(name)

def _bind_method(self, name, unconditionally=False):
"""Generate a Matlab function and bind it to the instance

This is where the magic happens. When an unknown attribute of the
Matlab class is requested, it is assumed to be a call to a
Matlab function, and is generated and bound to the instance.

This works because getattr() falls back to __getattr__ only if no
attributes of the requested name can be found through normal
routes (__getattribute__, __dict__, class tree).

bind_method first checks whether the requested name is a callable
Matlab function before generating a binding.

Parameters
----------
name : str
The name of the Matlab function to call
e.g. 'sqrt', 'sum', 'svd', etc
unconditionally : bool, optional
Bind the method without performing
checks. Used to bootstrap methods that are required and
know to exist

Returns
-------
MatlabFunction
A reference to a newly bound MatlabFunction instance if the
requested name is determined to be a callable function

Raises
------
AttributeError: if the requested name is not a callable
Matlab function

"""
# TODO: This does not work if the function is a mex function inside a folder of the same name
exists = self.run_func('exist', name)['result'] in [2, 3, 5]
if not unconditionally and not exists:
raise AttributeError("'Matlab' object has no attribute '%s'" % name)

# create a new method instance
method_instance = MatlabFunction(weakref.ref(self), name)
method_instance.__name__ = name

# bind to the Matlab instance with a weakref (to avoid circular references)
if sys.version.startswith('3'):
method = types.MethodType(method_instance, weakref.ref(self))
else:
method = types.MethodType(method_instance, weakref.ref(self),
_Session)
setattr(self, name, method)
return getattr(self, name)


class Matlab(_Session):
def __init__(self, executable='matlab', socket_addr=None,
Expand Down Expand Up @@ -433,3 +500,59 @@ def _preamble_code(self):

def _execute_flag(self):
return '--eval'


class MatlabFunction(object):

def __init__(self, parent, name):
"""An object representing a Matlab function

Methods are dynamically bound to instances of Matlab objects and
represent a callable function in the Matlab subprocess.

Parameters
----------
parent: Matlab instance
A reference to the parent (Matlab instance) to which the
MatlabFunction is being bound
name: str
The name of the Matlab function this represents
"""
self.name = name
self._parent = parent
self.doc = None

def __call__(self, unused_parent_weakref, *args, **kwargs):
"""Call a function with the supplied arguments in the Matlab subprocess

Passes parameters to `run_func`.

"""
return self.parent.run_func(self.name, *args, **kwargs)

@property
def parent(self):
"""Get the actual parent from the stored weakref

The parent (Matlab instance) is stored as a weak reference
to eliminate circular references from dynamically binding Methods
to Matlab.

"""
parent = self._parent()
if parent is None:
raise AttributeError('Stale reference to attribute of non-existent Matlab object')
return parent

@property
def __doc__(self):
"""Fetch the docstring from Matlab

Get the documentation for a Matlab function by calling Matlab's builtin
help() then returning it as the Python docstring. The result is cached
so Matlab is only ever polled on the first request

"""
if self.doc is None:
self.doc = self.parent.help(self.name)['result']
return self.doc
57 changes: 57 additions & 0 deletions pymatbridge/tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import numpy as np
import numpy.testing as npt
import test_utils as tu


class TestFunctions(object):

# Start a Matlab session before running any tests
@classmethod
def setup_class(cls):
cls.mlab = tu.connect_to_matlab()

# Tear down the Matlab session after running all the tests
@classmethod
def teardown_class(cls):
tu.stop_matlab(cls.mlab)

def test_nargout(self):
res = self.mlab.run_func('svd', np.array([[1,2],[1,3]]), nargout=3)
U, S, V = res['result']
npt.assert_almost_equal(U, np.array([[-0.57604844, -0.81741556],
[-0.81741556, 0.57604844]]))

npt.assert_almost_equal(S, np.array([[ 3.86432845, 0.],
[ 0., 0.25877718]]))

npt.assert_almost_equal(V, np.array([[-0.36059668, -0.93272184],
[-0.93272184, 0.36059668]]))

res = self.mlab.run_func('svd', np.array([[1,2],[1,3]]), nargout=1)
s = res['result']
npt.assert_almost_equal(s, [[ 3.86432845], [ 0.25877718]])

res = self.mlab.run_func('close', 'all', nargout=0)
assert res['result'] == []

def test_tuple_args(self):
res = self.mlab.run_func('ones', (1, 2))
npt.assert_almost_equal(res['result'], [[1, 1]])

res = self.mlab.run_func('chol',
np.array([[2, 2], [1, 1]]), 'lower')
npt.assert_almost_equal(res['result'],
[[1.41421356, 0.],
[0.70710678, 0.70710678]])

def test_create_func(self):
test = self.mlab.ones(3)
npt.assert_array_equal(test['result'], np.ones((3, 3)))
doc = self.mlab.zeros.__doc__
assert 'zeros' in doc

def test_pass_kwargs(self):
resp = self.mlab.run_func('plot', [1, 2, 3], Linewidth=3)
assert resp['success'] == 'true'
resp = self.mlab.plot([1, 2, 3], Linewidth=3)
assert resp['result'] is not None
30 changes: 0 additions & 30 deletions pymatbridge/tests/test_run_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,33 +64,3 @@ def test_undefined_code(self):
npt.assert_equal(message, "'this_is_nonsense' undefined near line 1 column 1")
else:
npt.assert_equal(message, "Undefined function or variable 'this_is_nonsense'.")


def test_nargout(self):
res = self.mlab.run_func('svd', np.array([[1,2],[1,3]]), nargout=3)
U, S, V = res['result']
npt.assert_almost_equal(U, np.array([[-0.57604844, -0.81741556],
[-0.81741556, 0.57604844]]))

npt.assert_almost_equal(S, np.array([[ 3.86432845, 0.],
[ 0., 0.25877718]]))

npt.assert_almost_equal(V, np.array([[-0.36059668, -0.93272184],
[-0.93272184, 0.36059668]]))

res = self.mlab.run_func('svd', np.array([[1,2],[1,3]]), nargout=1)
s = res['result']
npt.assert_almost_equal(s, [[ 3.86432845], [ 0.25877718]])

res = self.mlab.run_func('close', 'all', nargout=0)
assert res['result'] == []

def test_tuple_args(self):
res = self.mlab.run_func('ones', (1, 2))
npt.assert_almost_equal(res['result'], [[1, 1]])

res = self.mlab.run_func('chol',
(np.array([[2, 2], [1, 1]]), 'lower'))
npt.assert_almost_equal(res['result'],
[[1.41421356, 0.],
[0.70710678, 0.70710678]])