diff --git a/.travis.yml b/.travis.yml index 9d099c3..2b5a690 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: python -sudo: false deploy: provider: pypi user: arokem @@ -10,5 +9,35 @@ deploy: repo: arokem/python-matlab-bridge # until this is fixed: https://github.com/travis-ci/travis-ci/issues/1675 all_branches: true +env: + - CONDA="python=2.7 numpy=1.7" + - CONDA="python=3.3 numpy" +before_install: + - sudo apt-add-repository -y ppa:octave/stable; + - sudo apt-get update -qq; + - sudo apt-get install -qq octave liboctave-dev default-jdk; + - wget http://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; + - bash miniconda.sh -b -p $HOME/miniconda + - export PATH="$HOME/miniconda/bin:$PATH" + - hash -r + - conda config --set always_yes yes + - conda update conda + - conda info -a + - travis_retry conda create -n test $CONDA IPython pip nose pyzmq + - source activate test + - travis_retry pip install coveralls + +install: + - export USE_OCTAVE=True + - python setup.py install + script: - - true + # run coverage on py2.7, regular on others + - if [[ $CONDA == python=2.7* ]]; then + nosetests --exe -v --with-cov --cover-package pymatbridge; + else + nosetests --exe -v pymatbridge; + fi + +after_success: + - coveralls diff --git a/README.md b/README.md index 3aa3c6f..0e9a283 100644 --- a/README.md +++ b/README.md @@ -115,17 +115,7 @@ Rather than looking for `matlab` at the shell, this will look for `octave`. As with `pymatbridge.Matlab`, you can override this by specifying the `executable` keyword argument. -There are a few caveats to note about Octave support: - -* The zmq messenger used to communicate with the Octave session is written in C -and needs to be compiled. For MATLAB various prebuilt binaries are provided -and added to MATLAB's runtime path, but as of this writing the same isn't -true for Octave. You'll need to compile the messenger (in -`messenger/src/messenger.c`) using an incantation like -`mkoctfile --mex -lzmq messenger.c` and place the resulting `messenger.mex` -file somewhere in Octave's runtime path. - -Finally, rather than `~/startup.m`, Octave looks for an `~/.octaverc` file for +Rather than `~/startup.m`, Octave looks for an `~/.octaverc` file for commands to execute before every session. (This is a good place to manipulate the runtime path, for example). @@ -154,11 +144,11 @@ More examples are provided in the `examples` directory The installation of `pymatbridge` includes a binary of a mex function to communicate between Python and Matlab using the [0MQ](http://zeromq.org/) messaging library. This should work without any need for compilation on most computers. However, in some cases, you might want -to build the pymatbridge messenger from source. To do so, you will need to follow the +to build the pymatbridge messenger from source. To do so, you will need to follow the instructions below: ### Install zmq library Please refer to the [official guide](http://zeromq.org/intro:get-the-software) on how to -build and install zmq. After zmq is installed, make sure you can find the location where +build and install zmq. On Ubuntu, it is as simple as `sudo apt-get install libzmq3-dev`. After zmq is installed, make sure you can find the location where libzmq is installed. The library extension name and default location on different systems are listed below. @@ -215,12 +205,23 @@ be supported. If you have an old version of pyzmq, please update it. ### Install pymatbridge After the steps above are done, you can install pymatbridge. Download the zip file of the -latest release. Unzip it somewhere on your machine and then issue: +latest release. Unzip it somewhere on your machine. + +For Matlab: cd messenger python make.py cd .. python setup.py install + + +For Octave: + + cd messenger/octave + # edit make.py if not using Ubuntu-derivative + python make.py + cd .. + python setup.py This should make the python-matlab-bridge import-able. diff --git a/messenger/octave/make.py b/messenger/octave/make.py new file mode 100644 index 0000000..3ee5160 --- /dev/null +++ b/messenger/octave/make.py @@ -0,0 +1,7 @@ + +import os + +os.system('sudo apt-get install libzmq3-dev liboctave-dev') +os.system('cd ../src; mkoctfile --mex -lzmq messenger.c') +os.system('mv ../src/messenger.mex .') +os.system('cp messenger.mex ../pymatbridge/matlab') diff --git a/messenger/octave/messenger.mex b/messenger/octave/messenger.mex new file mode 100755 index 0000000..8a22d09 Binary files /dev/null and b/messenger/octave/messenger.mex differ diff --git a/pymatbridge/matlab/util/isrow.m b/pymatbridge/matlab/util/isrow.m index 767adef..12848f7 100644 --- a/pymatbridge/matlab/util/isrow.m +++ b/pymatbridge/matlab/util/isrow.m @@ -7,8 +7,8 @@ % % -if ndims(X)==2 & size(X,1)==1 & size(X,2)>=1 +if ndims(X)==2 && size(X,1)==1 && size(X,2)>=1 Y = logical(1); else Y = logical(0); -end \ No newline at end of file +end 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 ed8ef62..8f83aac 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 @@ -143,7 +143,7 @@ end elseif isnumeric(value) if isreal(value) - obj = javaObject('java.lang.Double', value); + obj = value; % Encode complex number as a struct else complex_struct = struct; diff --git a/pymatbridge/matlab/util/pymat_get_variable.m b/pymatbridge/matlab/util/pymat_get_variable.m index 9de9cf1..1b22952 100644 --- a/pymatbridge/matlab/util/pymat_get_variable.m +++ b/pymatbridge/matlab/util/pymat_get_variable.m @@ -34,6 +34,7 @@ else response.exists = true; response.var = evalin('base', varname); + response.success = 'true'; end json_response = json_dump(response); diff --git a/pymatbridge/matlab_magic.py b/pymatbridge/matlab_magic.py index d1e4251..ca8c326 100644 --- a/pymatbridge/matlab_magic.py +++ b/pymatbridge/matlab_magic.py @@ -7,36 +7,23 @@ """ -import sys, os -import tempfile -from glob import glob from shutil import rmtree -from getopt import getopt import numpy as np -try: - import scipy.io as sio - has_io = True -except ImportError: - has_io = False - no_io_str = "Must have scipy.io to perform i/o" - no_io_str += "operations with the Matlab session" - import IPython -ipython_version = int(IPython.__version__[0]) - from IPython.core.displaypub import publish_display_data -from IPython.core.magic import (Magics, magics_class, cell_magic, line_magic, +from IPython.core.magic import (Magics, magics_class, line_cell_magic, needs_local_scope) -from IPython.testing.skipdoctest import skip_doctest from IPython.core.magic_arguments import (argument, magic_arguments, parse_argstring) -from IPython.utils.py3compat import str_to_unicode, unicode_to_str, PY3 +from IPython.utils.py3compat import unicode_to_str, PY3 import pymatbridge as pymat from .compat import text_type +ipython_version = int(IPython.__version__[0]) + class MatlabInterperterError(RuntimeError): """ @@ -95,7 +82,10 @@ def __init__(self, shell, super(MatlabMagics, self).__init__(shell) self.cache_display_data = cache_display_data - self.Matlab = pymat.Matlab(matlab, maxtime=maxtime) + if 'octave' in matlab.lower(): + self.Matlab = pymat.Octave(matlab, maxtime=maxtime) + else: + self.Matlab = pymat.Matlab(matlab, maxtime=maxtime) self.Matlab.start() self.pyconverter = pyconverter @@ -173,19 +163,15 @@ def matlab(self, line, cell=None, local_ns=None): self.Matlab.set_default_plot_size(width, height) if args.input: - if has_io: - for input in ','.join(args.input).split(','): - try: - val = local_ns[input] - except KeyError: - val = self.shell.user_ns[input] - # The _Session.set_variable function which this calls - # should correctly detect numpy arrays and serialize them - # as json correctly. - self.set_matlab_var(input, val) - - else: - raise RuntimeError(no_io_str) + for input in ','.join(args.input).split(','): + try: + val = local_ns[input] + except KeyError: + val = self.shell.user_ns[input] + # The _Session.set_variable function which this calls + # should correctly detect numpy arrays and serialize them + # as json correctly. + self.set_matlab_var(input, val) try: result_dict = self.eval(code) @@ -216,7 +202,8 @@ def matlab(self, line, cell=None, local_ns=None): if len(imgf): # Store the path to the directory so that you can delete it # later on: - image = open(imgf, 'rb').read() + with open(imgf, 'rb') as fid: + image = fid.read() if ipython_version < 3: display_data.append(('MatlabMagic.matlab', {'image/png':image})) @@ -234,11 +221,8 @@ def matlab(self, line, cell=None, local_ns=None): rmtree(data_dir) if args.output: - if has_io: - for output in ','.join(args.output).split(','): - self.shell.push({output:self.Matlab.get_variable(output)}) - else: - raise RuntimeError(no_io_str) + for output in ','.join(args.output).split(','): + self.shell.push({output:self.Matlab.get_variable(output)}) _loaded = False @@ -253,6 +237,5 @@ def unload_ipython_extension(ip): global _loaded if _loaded: magic = ip.magics_manager.registry.pop('MatlabMagics') - magic.Matlab.stop() _loaded = False diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 543bc26..2e12f47 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -23,7 +23,7 @@ import os import time -import codecs +import base64 import zmq import subprocess import sys @@ -60,7 +60,7 @@ def encode_ndarray(obj): except AttributeError: data = obj.astype(float64).tostring() - data = codecs.encode(data, 'base64').decode('utf-8') + data = base64.b64encode(data).decode('utf-8') return data, shape @@ -89,19 +89,19 @@ def default(self, obj): def decode_arr(data): """Extract a numpy array from a base64 buffer""" data = data.encode('utf-8') - return frombuffer(codecs.decode(data, 'base64'), float64) + return frombuffer(base64.b64decode(data), float64) # JSON decoder for arrays and complex numbers def decode_pymat(dct): if 'ndarray' in dct and 'data' in dct: value = decode_arr(dct['data']) - shape = decode_arr(dct['shape']) + shape = decode_arr(dct['shape']).astype(int) return value.reshape(shape, order='F') elif 'ndarray' in dct and 'imag' in dct: real = decode_arr(dct['real']) imag = decode_arr(dct['imag']) - shape = decode_arr(dct['shape']) + shape = decode_arr(dct['shape']).astype(int) data = real + 1j * imag return data.reshape(shape, order='F') elif 'real' in dct and 'imag' in dct: @@ -428,6 +428,7 @@ def _preamble_code(self): if self.log: code.append("diary('./pymatbridge/logs/octavelog_%s.txt')" % self.id) code.append("set(0, 'defaultfigurevisible', 'off');") + code.append("graphics_toolkit('gnuplot')") return code def _execute_flag(self): diff --git a/pymatbridge/tests/test_magic.py b/pymatbridge/tests/test_magic.py index 57e3677..3b5d7f8 100644 --- a/pymatbridge/tests/test_magic.py +++ b/pymatbridge/tests/test_magic.py @@ -1,24 +1,30 @@ +import os + import pymatbridge as pymat -import IPython +from IPython.testing.globalipapp import get_ipython import numpy.testing as npt + class TestMagic: # Create an IPython shell and load Matlab magic @classmethod def setup_class(cls): - cls.ip = IPython.InteractiveShell() + cls.ip = get_ipython() cls.ip.run_cell('import random') cls.ip.run_cell('import numpy as np') - pymat.load_ipython_extension(cls.ip) + if 'USE_OCTAVE' in os.environ: + matlab = 'octave' + else: + matlab = 'matlab' + pymat.load_ipython_extension(cls.ip, matlab=matlab) - # Unload the magic, shut down Matlab + # Unload the magic @classmethod def teardown_class(cls): pymat.unload_ipython_extension(cls.ip) - # Test single operation on different data structures def test_cell_magic_number(self): # A double precision real number diff --git a/setup.py b/setup.py index 43dd4b7..acd6769 100755 --- a/setup.py +++ b/setup.py @@ -30,7 +30,8 @@ def copy_bin(bin_path): for copy_this in ["./messenger/mexmaci64/messenger.mexmaci64", "./messenger/mexa64/messenger.mexa64", - "./messenger/mexw64/messenger.mexw64"]: + "./messenger/mexw64/messenger.mexw64", + "./messenger/octave/messenger.mex"]: copy_bin(copy_this) # Get version and release info, which is all stored in pymatbridge/version.py