From f69e93c5f595cd59a2e59c3f0ae94a6060459f91 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 18:58:22 -0600 Subject: [PATCH 1/9] Add handling of dynamic methods --- pymatbridge/pymatbridge.py | 131 ++++++++++++++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 7659a12..028aee7 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -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 @@ -263,25 +265,31 @@ 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, *args, nargout=1, **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 + args: object Function args to send to the function. nargout: int 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' """ + 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, + func_args=args, nargout=nargout) def run_code(self, code): @@ -323,6 +331,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 + ------- + Method + A reference to a newly bound Method 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'] + if not unconditionally and not exists: + raise AttributeError("'Matlab' object has no attribute '%s'" % name) + + # create a new method instance + method_instance = Method(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, @@ -433,3 +499,62 @@ def _preamble_code(self): def _execute_flag(self): return '--eval' + + +# ---------------------------------------------------------------------------- +# MATLAB METHOD +# ---------------------------------------------------------------------------- +class Method(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. + + Args: + parent: A reference to the parent (Matlab instance) to which the + Method is being bound + name: The name of the Matlab function this represents + + """ + self.name = name + self._parent = parent + self.doc = None + + def __call__(self, unused_parent_weakref, *args, nargout=1, **kwargs): + """Call a function with the supplied arguments in the Matlab subprocess + + Passes parameters to `run_func`. + + """ + resp = self.parent.run_func(self.name, *args, **kwargs) + # return the result + return resp.get('result', None) + + @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) + return self.doc From 823c131f56ef2c576537ea349e489b992cb6eb3e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 18:58:33 -0600 Subject: [PATCH 2/9] Create new test_functions module and consolidate run_func tests --- pymatbridge/tests/test_functions.py | 59 +++++++++++++++++++++++++++++ pymatbridge/tests/test_run_code.py | 30 --------------- 2 files changed, 59 insertions(+), 30 deletions(-) create mode 100644 pymatbridge/tests/test_functions.py diff --git a/pymatbridge/tests/test_functions.py b/pymatbridge/tests/test_functions.py new file mode 100644 index 0000000..2dfdc1f --- /dev/null +++ b/pymatbridge/tests/test_functions.py @@ -0,0 +1,59 @@ +import pymatbridge as pymat +import random as rd +import numpy as np +import numpy.testing as npt +import test_utils as tu + + +class TestPrecision: + + # 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, 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 is not None diff --git a/pymatbridge/tests/test_run_code.py b/pymatbridge/tests/test_run_code.py index 2b7714f..abd903e 100644 --- a/pymatbridge/tests/test_run_code.py +++ b/pymatbridge/tests/test_run_code.py @@ -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]]) From 8eab630f2716cad2a85e8e70f356cd3b043c1244 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 18:59:14 -0600 Subject: [PATCH 3/9] Add error handling for dumping json data --- pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m index 8f83aac..c085226 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m @@ -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)); From 135cc470cf25f47d789f84a46b4748c5515e8720 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 19:05:45 -0600 Subject: [PATCH 4/9] Fix handling of nargout for python 2 --- pymatbridge/pymatbridge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 028aee7..33de8ee 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -265,7 +265,7 @@ 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, *args, nargout=1, **kwargs): + def run_func(self, func_path, *args, **kwargs): """Run a function in Matlab and return the result. Parameters @@ -285,6 +285,7 @@ def run_func(self, func_path, *args, nargout=1, **kwargs): ------- Result dictionary with keys: 'message', 'result', and 'success' """ + nargout = kwargs.pop('nargout', 1) args += tuple(item for pair in zip(kwargs.keys(), kwargs.values()) for item in pair) return self._json_response(cmd='run_function', @@ -522,7 +523,7 @@ def __init__(self, parent, name): self._parent = parent self.doc = None - def __call__(self, unused_parent_weakref, *args, nargout=1, **kwargs): + def __call__(self, unused_parent_weakref, *args, **kwargs): """Call a function with the supplied arguments in the Matlab subprocess Passes parameters to `run_func`. From 7fabbaca05e39b8cf2ba8ce245662aeb1545ee1d Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 19:11:53 -0600 Subject: [PATCH 5/9] Add error handling for dynamic methods --- pymatbridge/pymatbridge.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 33de8ee..b612f7b 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -530,6 +530,8 @@ def __call__(self, unused_parent_weakref, *args, **kwargs): """ resp = self.parent.run_func(self.name, *args, **kwargs) + if not resp['success'] == 'true': + raise ValueError(resp['message']) # return the result return resp.get('result', None) From 2dd1f618932a004bd8c5a681a6072224b7c843a4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 19:50:25 -0600 Subject: [PATCH 6/9] Clean up test_function --- pymatbridge/tests/test_functions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pymatbridge/tests/test_functions.py b/pymatbridge/tests/test_functions.py index 2dfdc1f..4a52b60 100644 --- a/pymatbridge/tests/test_functions.py +++ b/pymatbridge/tests/test_functions.py @@ -1,11 +1,9 @@ -import pymatbridge as pymat -import random as rd import numpy as np import numpy.testing as npt import test_utils as tu -class TestPrecision: +class TestFunctions(object): # Start a Matlab session before running any tests @classmethod From b974f9206a49f79e4253aa0f7b054a6ab9efeec8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Mon, 16 Feb 2015 20:53:41 -0600 Subject: [PATCH 7/9] Rename to MatlabFunction, fix docstring, and better check for exist --- pymatbridge/pymatbridge.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index b612f7b..fec7fe3 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -362,8 +362,8 @@ def _bind_method(self, name, unconditionally=False): Returns ------- - Method - A reference to a newly bound Method instance if the + MatlabFunction + A reference to a newly bound MatlabFunction instance if the requested name is determined to be a callable function Raises @@ -373,12 +373,12 @@ def _bind_method(self, name, unconditionally=False): """ # 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'] + 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 = Method(weakref.ref(self), name) + method_instance = MatlabFunction(weakref.ref(self), name) method_instance.__name__ = name # bind to the Matlab instance with a weakref (to avoid circular references) @@ -502,10 +502,7 @@ def _execute_flag(self): return '--eval' -# ---------------------------------------------------------------------------- -# MATLAB METHOD -# ---------------------------------------------------------------------------- -class Method(object): +class MatlabFunction(object): def __init__(self, parent, name): """An object representing a Matlab function @@ -513,11 +510,13 @@ def __init__(self, parent, name): Methods are dynamically bound to instances of Matlab objects and represent a callable function in the Matlab subprocess. - Args: - parent: A reference to the parent (Matlab instance) to which the - Method is being bound - name: The name of the Matlab function this represents - + 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 From 93b9d834b561ba88c69990d5b7c8795a0edf8ec3 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 17 Feb 2015 14:22:15 -0600 Subject: [PATCH 8/9] Return the full structure in the dynamic functions --- pymatbridge/pymatbridge.py | 8 ++------ pymatbridge/tests/test_functions.py | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index fec7fe3..2155be6 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -528,11 +528,7 @@ def __call__(self, unused_parent_weakref, *args, **kwargs): Passes parameters to `run_func`. """ - resp = self.parent.run_func(self.name, *args, **kwargs) - if not resp['success'] == 'true': - raise ValueError(resp['message']) - # return the result - return resp.get('result', None) + return self.parent.run_func(self.name, *args, **kwargs) @property def parent(self): @@ -558,5 +554,5 @@ def __doc__(self): """ if self.doc is None: - self.doc = self.parent.help(self.name) + self.doc = self.parent.help(self.name)['result'] return self.doc diff --git a/pymatbridge/tests/test_functions.py b/pymatbridge/tests/test_functions.py index 4a52b60..4fed51c 100644 --- a/pymatbridge/tests/test_functions.py +++ b/pymatbridge/tests/test_functions.py @@ -46,7 +46,7 @@ def test_tuple_args(self): def test_create_func(self): test = self.mlab.ones(3) - npt.assert_array_equal(test, np.ones((3, 3))) + npt.assert_array_equal(test['result'], np.ones((3, 3))) doc = self.mlab.zeros.__doc__ assert 'zeros' in doc @@ -54,4 +54,4 @@ 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 is not None + assert resp['result'] is not None From a176ba95714e7006d970eea4d589cd0a61429a12 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 17 Feb 2015 14:27:10 -0600 Subject: [PATCH 9/9] Update parameter name and docstring --- pymatbridge/pymatbridge.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 2155be6..c69120e 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -265,16 +265,16 @@ 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, *args, **kwargs): + 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. - 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 @@ -286,11 +286,11 @@ def run_func(self, func_path, *args, **kwargs): Result dictionary with keys: 'message', 'result', and 'success' """ nargout = kwargs.pop('nargout', 1) - args += tuple(item for pair in zip(kwargs.keys(), kwargs.values()) - for item in pair) + 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=args, + func_args=func_args, nargout=nargout) def run_code(self, code):