diff --git a/.gitignore b/.gitignore index bb42edb..5bd58c1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,6 @@ ADME.markdown *~ build -# a log directory made for testing. -pymatbridge/matlab/www/log/ # a nonce file to push this package to our local system. dpush.sh @@ -36,3 +34,6 @@ rebuildTags.sh # binary file in matlab folder /pymatbridge/matlab/messenger.* + +# caused py setup.py develop +pymatbridge.egg-info diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..72a4326 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: python +deploy: + provider: pypi + user: arokem + password: + secure: AB7BnrkNOTQWXUNiaShedbIEBay+5JbulAxUHY4vEMdiI68bPLYOrIDxuT/lIfWrrgXUuicbJcWUxt6zpdUEQrEfYB6pNhrosZv4+R5HZu1FHY7EfOfsyHxb2wnezKuIEVXdevAmIg5rYNeTPcdl/CFm4xQGfdc4a3eyT5cJT58= + on: + tags: true + 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" + - CONDA="python=3.4 numpy" +before_install: + - sudo apt-add-repository -y ppa:octave/stable; + - sudo apt-get update; + - sudo apt-get install 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 jsonschema + - source activate test + - if [[ $CONDA == python=3.3* ]]; then + pip install nbformat; + else + travis_retry conda install nbformat; + fi + - travis_retry pip install coveralls + +install: + - export USE_OCTAVE=True + - python setup.py install + +script: + # 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 47dee20..0619ff8 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,42 @@ -# Python-MATLAB(R) Bridge and ipython matlab_magic +# Python-MATLAB(R) Bridge and IPython `matlab` magic + +[![Build Status](https://travis-ci.org/arokem/python-matlab-bridge.svg?branch=master)](https://travis-ci.org/arokem/python-matlab-bridge) +[![Coverage Status](https://coveralls.io/repos/arokem/python-matlab-bridge/badge.svg?branch=master)](https://coveralls.io/r/arokem/python-matlab-bridge?branch=master) +[![Latest Version](https://pypip.in/version/pymatbridge/badge.svg?style=flat)](https://pypi.python.org/pypi/pymatbridge/) +[![License](https://pypip.in/license/pymatbridge/badge.svg?style=flat)](https://pypi.python.org/pypi/pymatbridge/) +[![Join the chat at https://gitter.im/arokem/python-matlab-bridge](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/arokem/python-matlab-bridge?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) A python interface to call out to [Matlab(R)](http://mathworks.com). Original -implementation by [Max Jaderberg](http://www.maxjaderberg.com/). +implementation by [Max Jaderberg](http://www.maxjaderberg.com/). His original +repo of the project can be found [here]( +https://github.com/jaderberg/python-matlab-bridge), but please note that the +development of the two repositories has significantly diverged. This implementation also includes an [IPython](http://ipython.org) `matlab_magic` extension, which provides a simple interface for weaving python and -Matlab code together (requires ipython > 0.13). - +Matlab code together (requires ipython > 0.13). -***AT PRESENT THIS DOES NOT WORK ON WINDOWS*** ## Installation -Pymatbridge communicates with Matlab using zeromq. So before installing -pymatbridge you must have [zmq](http://zeromq.org/intro:get-the-software) -library and [pyzmq](http://zeromq.org/bindings:python) installed on your -machine. If you intend to use the Matlab magic extension, you'll also need -[IPython](http://ipython.org/install.html). To make pymatbridge work properly, -please follow the following steps. - -### 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 -libzmq is installed. The library extension name and default location on different systems -are listed below. - -| Platform | library name | Default locations | -| ------------- | ------------- | -------------------------- | -| MacOS | libzmq.dylib | /usr/lib or /usr/local/lib | -| Linux | libzmq.so.3 | /usr/lib or /usr/local/lib | -| Windows | libzmq.dll | TBD | - -If you specified a prefix when installing zmq, the library file should be located at the -same prefix location. - -After the library file is located, you need to add it to dynamic loader path on your -machine. On MacOS, you can do this by adding the following line to your .bash_profile: - - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH: +`pymatbridge` can be installed from [PyPI][1]: -On Linux, add the following line to your .bash_profile: - - export LD_LIBRARY_PATH=$DYLD_LIBRARY_PATH: - -On Windows, - - TBD - -### Install pyzmq -After step 1 is finished, please grab the latest version of -[pyzmq](http://zeromq.org/bindings:python) and follow the instructions on the official -page. Note that pymatbridge is developed with pyzmq 14.0.0 and older versions might not -be supported. If you have an old version of pyzmq, please update it. +``` +$ pip install pymatbridge +``` -### 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: +If you intend to use the Matlab magic extension, you'll also need +[IPython](http://ipython.org/install.html). - python setup.py install - -This should make the python-matlab-bridge import-able. +Finally, if you want to handle sparse arrays, you will need to install +[Scipy](http://scipy.org/). This can also be installed from PyPI, or using +distributions such as [Anaconda](https://store.continuum.io/cshop/anaconda/) or +[Enthought Canopy](https://store.enthought.com/downloads/) ## Usage -To use the pymatbridge you need to connect your python interperter to a Matlab +To use the pymatbridge you need to connect your python interpreter to a Matlab session. This is done in the following manner: from pymatbridge import Matlab @@ -74,18 +46,18 @@ This creates a matlab session class instance, into which you will be able to inject code and variables, and query for results. By default, when you use `start`, this will open whatever gets called when you type `matlab` in your Terminal, but you can also specify the location of your Matlab -application when initialzing your matlab session class: +application when initializing your matlab session class: + + mlab = Matlab(executable='/Applications/MATLAB_R2011a.app/bin/matlab') - mlab = Matlab(matlab='/Applications/MATLAB_R2011a.app/bin/matlab') - You can then start the Matlab server, which will kick off your matlab session, -and create the connection between your Python interperter and this session: +and create the connection between your Python interpreter and this session: mlab.start() which will return True once connected. - results = mlab.run_code('a=1;') + results = mlab.run_code('a=1') Should now run that line of code and return a `results` dict into your Python namespace. The `results` dict contains the following fields: @@ -100,8 +72,8 @@ In this case, the variable `a` is available on the Python side, by using the `get_variable` method: mlab.get_variable('a') - -You can run any MATLAB functions contained within a .m file of the + +You can run any MATLAB functions contained within a .m file of the same name. For example, to call the function jk in jk.m: %% MATLAB @@ -114,7 +86,7 @@ same name. For example, to call the function jk in jk.m: you would call: res = mlab.run_func('path/to/jk.m', {'arg1': 3, 'arg2': 5}) - print res['result'] + print(res['result']) This would print `8`. @@ -125,33 +97,152 @@ You can shut down the MATLAB server by calling: Tip: you can execute MATLAB code at the beginning of each of your matlab sessions by adding code to the `~/startup.m` file. +### Octave support & caveats + +A `pymatbridge.Octave` class is provided with exactly the same interface +as `pymatbridge.Matlab`: + + from pymatbridge import Octave + octave = Octave() + +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. + +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). -### Matlab magic: +Requires Version 3.8 or higher. Notice: Neither the MXE 3.8.1 nor the Cygwin 3.8.2 version is compatible on Windows. No Windows support will be available +until a working version of Octave 3.8+ with Java support is released. + + +### Matlab magic: The Matlab magic allows you to use pymatbridge in the context of the IPython notebook format. - import pymatbridge as pymat - ip = get_ipython() - pymat.load_ipython_extension(ip) + %load_ext pymatbridge These lines will automatically start the matlab session for you. Then, you can simply decorate a line/cell with the '%matlab' or '%%matlab' decorator and write matlab code: - %%matlab + %%matlab a = linspace(0.01,6*pi,100); plot(sin(a)) grid on hold on plot(cos(a),'r') +If `%load_ext pymatbridge` doesn't work for you use: +``` +import pymatbridge as pymat +pymat.load_ipython_extension(get_ipython(), matlab='/your_matlab_installation_dir/bin/matlab') +``` + More examples are provided in the `examples` directory - + +## Building the pymatbridge messenger from source + +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 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. On Ubuntu, it is as simple as `sudo apt-get install libzmq3-dev`. +On Windows, suggest using the following method: +- Install [MSYS2](http://sourceforge.net/projects/msys2/) +- Run `$ pacman -S make` +- From the zmq source directory, run: `$ sh configure --prefix=$(pwd) --build=x86_64-w64-mingw32` +- Run `$ make`. + +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. + +| Platform | library name | Default locations | +| ------------- | ------------- | --------------------------------- | +| MacOS | libzmq.dylib | /usr/lib or /usr/local/lib | +| Linux | libzmq.so.3 | /usr/lib or /usr/local/lib | +| Windows | libzmq.dll | C:\Program Files\ZeroMQ 3.2.4\bin | + +If you specified a prefix when installing zmq, the library file should be located at the +same prefix location. + +The pymatbridge MEX extension needs to be able to locate the zmq library. If it's in a +standard location, you may not need to do anything; if not, there are two ways to +accomplish this: + +#### Using the dynamic loader path + +One option is to set an environment variable which will point the loader to the right +directory. + +On MacOS, you can do this by adding the following line to your .bash_profile (or similar +file for your shell): + + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH: + +On Linux, add the following line to your .bash_profile (or similar file for your shell): + + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH: + +On Windows, add the install location of libzmq.dll to the PATH environment variable. +On Windows 7+, typing "environment variables" into the start menu will bring up the +apporpriate Control Panel links. + +#### Pointing the binary at the right place + +Another option is to modify the MEX binary to point to the right location. This is +preferable in that it doesn't change loader behavior for other libraries than just +the pymatbridge messenger. + +On MacOS, you can do this from the root of the pymatbridge code with: + + install_name_tool -change /usr/local/lib/libzmq.3.dylib /libzmq.3.dylib messenger/maci64/messenger.mexmaci64 + +On Linux, you can add it to the RPATH: + + patchelf --set-rpath messenger/mexa64/messenger.mexa64 + +### Install pyzmq +After step 1 is finished, please grab the latest version of +[pyzmq](http://zeromq.org/bindings:python) and follow the instructions on the official +page. Note that pymatbridge is developed with pyzmq 14.0.0 and older versions might not +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. + +For Matlab: + + cd messenger + # edit local.cfg in the directory for your platform + python make.py matlab + cd .. + python setup.py install + + +For Octave: + + cd messenger/octave + # edit local_octave.cfg in the directory for your platform + python make.py octave + cd .. + python setup.py + +This should make the python-matlab-bridge import-able. + + ### Warnings Python communicates with Matlab via an ad-hoc zmq messenger. This is inherently insecure, as the Matlab instance may be directed to perform arbitrary system calls. There is no sandboxing of any kind. Use this code at your own risk. - +[1]: https://pypi.python.org/pypi/pymatbridge diff --git a/THANKS b/THANKS deleted file mode 100644 index b30b67c..0000000 --- a/THANKS +++ /dev/null @@ -1,8 +0,0 @@ -## Contributors: - -Max Jaderberg -Ariel Rokem -Steven Pav -Jens Nielsen - -We rely on an adaptation of [webserver code written by Dirk Jan Kroon](http://www.mathworks.com/matlabcentral/fileexchange/29027-web-server). diff --git a/messenger/mexa64/local.cfg b/messenger/mexa64/local.cfg deleted file mode 100644 index bc419a2..0000000 --- a/messenger/mexa64/local.cfg +++ /dev/null @@ -1,3 +0,0 @@ -MATLAB_BIN=/white/local/matlab/r2012b/bin -HEADER_PATH=/home/haoxingz/zmq/include -LIB_PATH=/home/haoxingz/zmq/lib diff --git a/messenger/mexa64/make.py b/messenger/mexa64/make.py deleted file mode 100755 index d76b675..0000000 --- a/messenger/mexa64/make.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python - -import os -import sys -import fnmatch -import subprocess - -# Check the system platform first -platform = sys.platform -print "This is a " + platform + " system" - -# Open the configure file and start parsing -config = open('local.cfg', 'r') - -for line in config: - path = line.split('=') - - if path[0] == "MATLAB_BIN": - print "Searching for Matlab bin folder in local.cfg ..." - matlab_bin = path[1].rstrip('\r\n') - if matlab_bin == "": - raise ValueError("Could not find Matlab bin folder. Please add it to local.cfg") - print "Matlab found in " + matlab_bin - - elif path[0] == "HEADER_PATH": - print "Searching for zmq.h in local.cfg ..." - header_path = path[1].rstrip('\r\n') - if header_path == "": - raise ValueError("Could not find zmq.h. Please add its path to local.cfg") - print "zmq.h found in " + header_path - - elif path[0] == "LIB_PATH": - print "Searching for zmq library in local.cfg ..." - lib_path = path[1].rstrip('\r\n') - if lib_path == "": - raise ValueError("Could not find zmq library. Please add its path to local.cfg") - - print "zmq library found in " + lib_path - -config.close() - -# Get the extension -if platform == 'win32': - extcmd = '"' + matlab_bin + "\\mexext.bat" + '"' - check_extension = subprocess.Popen(extcmd, stdout = subprocess.PIPE) - extension = check_extension.stdout.readline().rstrip('\r\n') -else: - extcmd = matlab_bin + "/mexext" - check_extension = subprocess.Popen(extcmd, stdout = subprocess.PIPE) - extension = check_extension.stdout.readline().rstrip('\r\n') - -print "Building messenger." + extension + " ..." - -# Build the mex file -if platform == 'win32': - mex = "\\mex.bat" -else: - mex = "/mex" -make_cmd = '"' + matlab_bin + mex + '"' + " -v -I" + header_path + " -L" + lib_path + " -lzmq ../src/messenger.c" -os.system(make_cmd) - diff --git a/messenger/mexa64/messenger.mexa64 b/messenger/mexa64/messenger.mexa64 deleted file mode 100755 index eaf2c7e..0000000 Binary files a/messenger/mexa64/messenger.mexa64 and /dev/null differ diff --git a/messenger/mexmaci64/local.cfg b/messenger/mexmaci64/local.cfg deleted file mode 100644 index d8d9ff6..0000000 --- a/messenger/mexmaci64/local.cfg +++ /dev/null @@ -1,3 +0,0 @@ -MATLAB_BIN=/Applications/MATLAB_R2012a.app/bin -HEADER_PATH=/usr/include -LIB_PATH=/usr/local/lib diff --git a/messenger/mexmaci64/make.py b/messenger/mexmaci64/make.py deleted file mode 100755 index 8b82177..0000000 --- a/messenger/mexmaci64/make.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/python - -import os -import sys -import fnmatch -import subprocess - -# Check the system platform first -platform = sys.platform -print "This is a " + platform + " system" - -# Open the configure file and start parsing -config = open('local.cfg', 'r') - -for line in config: - path = line.split('=') - - if path[0] == "MATLAB_BIN": - print "Searching for Matlab bin folder in local.cfg ..." - matlab_bin = path[1].rstrip('\r\n') - if matlab_bin == "": - raise ValueError("Could not find Matlab bin folder. Please add it to local.cfg") - print "Matlab found in " + matlab_bin - - elif path[0] == "HEADER_PATH": - print "Searching for zmq.h in local.cfg ..." - header_path = path[1].rstrip('\r\n') - if header_path == "": - raise ValueError("Could not find zmq.h. Please add its path to local.cfg") - print "zmq.h found in " + header_path - - elif path[0] == "LIB_PATH": - print "Searching for zmq library in local.cfg ..." - lib_path = path[1].rstrip('\r\n') - if lib_path == "": - raise ValueError("Could not find zmq library. Please add its path to local.cfg") - - print "zmq library found in " + lib_path - -config.close() - -# Get the extension -if platform == 'win32': - extcmd = '"' + matlab_bin + "\\mexext.bat" + '"' - check_extension = subprocess.Popen(extcmd, stdout = subprocess.PIPE) - extension = check_extension.stdout.readline().rstrip('\r\n') -else: - extcmd = matlab_bin + "/mexext" - check_extension = subprocess.Popen(extcmd, stdout = subprocess.PIPE) - extension = check_extension.stdout.readline().rstrip('\r\n') - -print "Building messenger." + extension + " ..." - -# Build the mex file -if platform == 'win32': - mex = "\\mex.bat" -else: - mex = "/mex" -make_cmd = '"' + matlab_bin + mex + '"' + " -O -I" + header_path + " -L" + lib_path + " -lzmq ../src/messenger.c" -os.system(make_cmd) - diff --git a/messenger/mexmaci64/messenger.mexmaci64 b/messenger/mexmaci64/messenger.mexmaci64 deleted file mode 100755 index b744df7..0000000 Binary files a/messenger/mexmaci64/messenger.mexmaci64 and /dev/null differ diff --git a/messenger/mexw32/local.cfg b/messenger/mexw32/local.cfg deleted file mode 100644 index d8d9ff6..0000000 --- a/messenger/mexw32/local.cfg +++ /dev/null @@ -1,3 +0,0 @@ -MATLAB_BIN=/Applications/MATLAB_R2012a.app/bin -HEADER_PATH=/usr/include -LIB_PATH=/usr/local/lib diff --git a/messenger/mexw64/local.cfg b/messenger/mexw64/local.cfg deleted file mode 100644 index d8d9ff6..0000000 --- a/messenger/mexw64/local.cfg +++ /dev/null @@ -1,3 +0,0 @@ -MATLAB_BIN=/Applications/MATLAB_R2012a.app/bin -HEADER_PATH=/usr/include -LIB_PATH=/usr/local/lib diff --git a/pymatbridge/__init__.py b/pymatbridge/__init__.py index 3ad8122..45d2145 100644 --- a/pymatbridge/__init__.py +++ b/pymatbridge/__init__.py @@ -1,6 +1,12 @@ -from pymatbridge import * +from .pymatbridge import * +from .version import __version__ try: - from matlab_magic import * + from .publish import * +except ImportError: + pass + +try: + from .matlab_magic import * except ImportError: pass diff --git a/pymatbridge/compat.py b/pymatbridge/compat.py new file mode 100644 index 0000000..bca21fc --- /dev/null +++ b/pymatbridge/compat.py @@ -0,0 +1,10 @@ +import sys + +PY3 = sys.version_info[0] == 3 + +if PY3: + text_type = str + unichr = chr +else: + text_type = unicode + unichr = unichr diff --git a/pymatbridge/examples/matlab_magic.ipynb b/pymatbridge/examples/matlab_magic.ipynb index 002e76c..8b9e1b3 100644 --- a/pymatbridge/examples/matlab_magic.ipynb +++ b/pymatbridge/examples/matlab_magic.ipynb @@ -1,144 +1,152 @@ { - "metadata": { - "name": "", - "signature": "sha256:b7fe332441992a55b1e28dc444bd81f86bbf15a75134d851ec37ce17d19c57e5" - }, - "nbformat": 3, - "nbformat_minor": 0, - "worksheets": [ + "cells": [ { - "cells": [ - { - "cell_type": "code", - "collapsed": false, - "input": [ - "import pymatbridge as pymat" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": 1 - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "ip = get_ipython()\n", - "pymat.load_ipython_extension(ip)" - ], - "language": "python", - "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "Starting MATLAB on ZMQ socket ipc:///tmp/pymatbridge\n", - "Send 'exit' command to kill the server\n", - "." - ] - }, - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "MATLAB started and connected!\n" - ] - } - ], - "prompt_number": 2 - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "%%matlab\n", - "t = linspace(0,6*pi,100);\n", - "plot(sin(t))\n", - "grid on\n", - "hold on\n", - "plot(cos(t), 'r')" - ], - "language": "python", - "metadata": {}, - "outputs": [ - { - "metadata": {}, - "output_type": "display_data", - "png": "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\nUWAAgJD+B77HMO0L8V3/AAAAAElFTkSuQmCC\n" - } - ], - "prompt_number": 3 - }, - { - "cell_type": "code", - "collapsed": false, - "input": [ - "%matlab b = 10*cos(a)+30; plot(b); grid on" - ], - "language": "python", - "metadata": {}, - "outputs": [ - { - "metadata": {}, - "output_type": "display_data", - "png": "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\nm0QLgAAAAABJRU5ErkJggg==\n" - } - ], - "prompt_number": 4 - }, + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import pymatbridge as pymat" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "a = [1,2,3]\n" - ], - "language": "python", - "metadata": {}, - "outputs": [], - "prompt_number": 8 - }, + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting MATLAB on ZMQ socket ipc:///tmp/pymatbridge-13a5236e-5f5a-4b58-a712-84cca5314f75\n", + "Send 'exit' command to kill the server\n", + "..............MATLAB started and connected!\n" + ] + } + ], + "source": [ + "ip = get_ipython()\n", + "pymat.load_ipython_extension(ip)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "%%matlab -i a -o b\n", - "b = a +3;" - ], - "language": "python", + "data": { + "image/png": "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\n" + }, "metadata": {}, - "outputs": [], - "prompt_number": 10 - }, + "output_type": "display_data" + } + ], + "source": [ + "%%matlab --size 400,350\n", + "t = linspace(0,6*pi,100);\n", + "plot(sin(t))\n", + "grid on\n", + "hold on\n", + "plot(cos(t), 'r')" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "print(b)" - ], - "language": "python", + "data": { + "image/png": "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\n" + }, "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "[4, 5, 6]\n" - ] - } - ], - "prompt_number": 11 - }, + "output_type": "display_data" + } + ], + "source": [ + "%matlab b = 10*cos(t)+30; plot(b); grid on" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "a = [1,2,3]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "%%matlab -i a -o b\n", + "b = a + 3;" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [], - "language": "python", - "metadata": {}, - "outputs": [] + "name": "stdout", + "output_type": "stream", + "text": [ + "[[ 4. 5. 6.]]\n" + ] } ], - "metadata": {} + "source": [ + "print(b)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [] } - ] -} \ No newline at end of file + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.4.3" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pymatbridge/examples/pymatbridge.ipynb b/pymatbridge/examples/pymatbridge.ipynb index b582827..ff116fb 100644 --- a/pymatbridge/examples/pymatbridge.ipynb +++ b/pymatbridge/examples/pymatbridge.ipynb @@ -1,250 +1,250 @@ { - "metadata": { - "name": "pymatbridge" - }, - "nbformat": 3, - "nbformat_minor": 0, - "worksheets": [ + "cells": [ { - "cells": [ + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import os\n", + "import numpy as np\n", + "import pymatbridge as pymat" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "import os\n", - "import numpy as np\n", - "import pymatbridge as pymat\n", - "reload(pymat)" - ], - "language": "python", - "metadata": {}, - "outputs": [ - { - "output_type": "pyout", - "prompt_number": 1, - "text": [ - "" - ] - } - ], - "prompt_number": 1 + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting MATLAB on ZMQ socket ipc:///tmp/pymatbridge-051089d9-1f6e-4298-8228-e9e1a8b3f368\n", + "Send 'exit' command to kill the server\n", + ".......MATLAB started and connected!\n" + ] }, { - "cell_type": "code", - "collapsed": false, - "input": [ - "matlab = pymat.Matlab()\n", - "matlab.start()" - ], - "language": "python", + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "woot\n", - "Starting MATLAB on http://localhost:51435\n", - " visit http://localhost:51435/exit.m to shut down same\n" - ] - }, - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "." - ] - }, - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "." - ] - }, - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "." - ] - }, - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "MATLAB started and connected!\n" - ] - }, - { - "output_type": "pyout", - "prompt_number": 2, - "text": [ - "True" - ] - } - ], - "prompt_number": 2 - }, + "output_type": "execute_result" + } + ], + "source": [ + "matlab = pymat.Matlab()\n", + "matlab.start()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "res = matlab.run_func('%s/matlab/www/demo_func.m'%os.path.dirname(pymat.__file__), {'a': 10})\n", - "res['result']" - ], - "language": "python", + "data": { + "text/plain": [ + "11" + ] + }, + "execution_count": 3, "metadata": {}, - "outputs": [ - { - "output_type": "pyout", - "prompt_number": 3, - "text": [ - "11" - ] - } - ], - "prompt_number": 3 - }, + "output_type": "execute_result" + } + ], + "source": [ + "func_file = os.path.dirname(pymat.__file__) + '/matlab/www/demo_func.m'\n", + "res = matlab.run_func(func_file, {'a': 10})\n", + "res['result']" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "res_dict = matlab.run_code('a=[1,3,7]')\n", - "res_dict = matlab.run_code('plot(a)')\n", - "res_dict = matlab.run_code('b=a+1')\n", - "exec('this=np.array(%s)'%matlab.get_variable('b')) \n", - "this" - ], - "language": "python", + "data": { + "text/plain": [ + "array([[ 2., 4., 8.]])" + ] + }, + "execution_count": 4, "metadata": {}, - "outputs": [ - { - "output_type": "pyout", - "prompt_number": 4, - "text": [ - "array([2, 4, 8])" - ] - } - ], - "prompt_number": 4 - }, + "output_type": "execute_result" + } + ], + "source": [ + "res_dict = matlab.run_code('a=[1,3,7]')\n", + "res_dict = matlab.run_code('plot(a)')\n", + "res_dict = matlab.run_code('b=a+1')\n", + "matlab.get_variable('b')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [ - "res_dict = matlab.run_code('figure; plot(b); b')\n", - "\n", - "from IPython.core.displaypub import publish_display_data\n", - "imgfiles = [res_dict['content']['figures']]\n", - "text_output = res_dict['content']['stdout']\n", - "\n", - "display_data = []\n", - "if text_output:\n", - " display_data.append(('MatlabMagic.matlab',\n", - " {'text/plain':text_output}))\n", - "for imgf in imgfiles:\n", - " image = open(imgf[0], 'rb').read() \n", - " display_data.append(('MatlabMagic.matlab', {'image/png': image}))\n", - "\n", - "for tag, disp_d in display_data:\n", - " publish_display_data(tag, disp_d)\n", - "\n" - ], - "language": "python", + "data": { + "text/plain": [ + "\n", + "b =\n", + "\n", + " 2 4 8\n", + "\n" + ] + }, "metadata": {}, - "outputs": [ - { - "output_type": "display_data", - "text": [ - "\n", - "b =\n", - "\n", - " 2 4 8\n", - "\n" - ] - }, - { - "output_type": "display_data", - "png": "iVBORw0KGgoAAAANSUhEUgAAAkAAAAGwCAIAAADOgk3lAAAACXBIWXMAAAsSAAALEgHS3X78AAAA\nIXRFWHRTb2Z0d2FyZQBBcnRpZmV4IEdob3N0c2NyaXB0IDguNTRTRzzSAAANX0lEQVR4nO3c0XLi\nSBJA0aqJ/v9f1j7QyzAYMAipKjPrnIeJ7vZEKI2FrlPG9G3bGgBk88/sAQBgDwEDICUBAyAlAQMg\nJQEDICUBAyAlAQMgJQEDICUBAyAlAQMgJQEDICUBAyAlAQMgJQEDICUBAyAlAQMgJQEDICUBAyAl\nAQMgJQEDICUBAyAlAQMgJQEDICUBAyAlAQMgJQEDICUBAyAlAQMgpWoB67333mdPAcDpSgWs975t\n27ZtGgZQXqmAAbCOP7MHONhl99q27favAHzoeh2Nq07ALvcPf/45whfhdp7Fx4gwgzGizRBkjAgz\nBBmj99Zab23+o/GaW4gA/Kv3Njug76qzgd2+dmP69y8AGSWqV6sUsKZbAF/IVa/W2vybraeKcDcZ\nIL67eqW4ePoZGMDq0u1eFwIGsLSk9WoCBrCyvPVqAgawrNT1agIGsKbs9WoCBrCgAvVqAgawmhr1\nagIGsJQy9WoCBrCOSvVqAgawiGL1agIGsIJ69WoCBkBSAgZQXMn1qwkYQG1V69UEDKCwwvVqAgZQ\nVe16NQEDKKl8vZqAAdSzQr2agAEUs0i9moABVLJOvZqAAZSxVL2agAHUsFq9moABFLBgvZqAAWS3\nZr2agAGktmy9moAB5LVyvZqAASS1eL2agAFkpF5NwADSUa8LAQPIRL2uBAwgDfW6JWAAOajXHQED\nICUBA0jA+vWTgAFEp14PCRhAaOr1jIABxKVeLwgYQFDq9ZqAAUSkXr8SMIBw1OsdAgYQi3q9ScAA\nAlGv9wkYQBTq9REBAwhBvT4lYADzqdcOAgYwmXrtI2AAM6nXbgIGMI16fUPAAOZQry8JGMAE6vW9\nP7MHOEzv/favm1MDiEq9DlEnYLfFuosZQBzqdZSCtxB779YvgPLqbGDPuLUIxBF2/cp446pawH6u\nX4oFBBG2Xu3HpTJFzwreQgQIKHK9kioVMD/9AmJSrzOUCph6AQGp10lKBQwgGvU6j4ABnEW9TiVg\nAKdQr7MJGMDx1GsAAQM4mHqNIWAAR1KvYQQM4DDqNZKAARxDvQYTMIADqNd4AgbwLfWaQsAAvqJe\n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\nAAAAAElFTkSuQmCC\n" - } - ], - "prompt_number": 5 + "output_type": "display_data" }, { - "cell_type": "code", - "collapsed": false, - "input": [ - "matlab.is_connected()" - ], - "language": "python", + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgAAAAGACAIAAABUQk3oAAAACXBIWXMAABcSAAAXEgFnn9JSAAAA\nB3RJTUUH3wgFAjkLssA/9QAAACR0RVh0U29mdHdhcmUATUFUTEFCLCBUaGUgTWF0aFdvcmtzLCBJ\nbmMuPFjdGAAAACJ0RVh0Q3JlYXRpb24gVGltZQAwNC1BdWctMjAxNSAyMjo1NzoxMWrauvUAAB7C\nSURBVHic7d1/UFT3/e/xj6gRwVJqTRV20NWkK/4WRDJh/EmIWsZfTdtv0lRMYIy/gExTyThqEyE3\nnanJ3DiKgzVOow6mMfUHI+3NTYISaidcxYBCKhZBwSYLKgk6JuGH6673j5NuCCIuy+5+Puec5+Ov\n9cg6755u3i9gz9lXvzt37ggAgPkEyR4AACAHAQAAJkUAAIBJEQAAYFIEAACYFAEAACZFAACASREA\nAGBSBAAAmBQBAAAmRQAAgEkRAABgUgQAAJgUAQAAJkUAAIBJEQAAYFIEAACYFAEAACZFAACASQ2Q\nPUD3Tp06dfr06StXrgQHB8fGxs6ZMyckJET2UABgKP1UK4VvaWlZs2bN2bNnOx8MCwt74403Zs6c\nKWsqADAe5X4FtG7durNnz06bNu3QoUP/+te/Tpw4sXr16ps3b2ZkZDQ2NsqeDgCMQ60AaGlpKS0t\nHTBgQF5e3qRJkwYOHDh8+PAXXnhhzpw57e3tRUVFsgcEAONQKwD+/e9/CyHCw8PDw8M7H4+LixNC\n1NfXyxkLAIxIrQCYPn36gAEDvv76a4fD0fn4hQsXxH9jAADgE2oFwMCBA9PT09vb29evX3/jxg0h\nhMvlOnz4cGFh4fjx4+fPny97QAAwDuWuAhJCFBQU/PnPf7506VJwcLDD4Rg0aNCvfvWrNWvWhIWF\nyR4NAIxDufsArl69WlhYWFtbGxkZabPZbty4cfbs2ffeey8+Pn7u3Ln3elZubm5ZWVkg5wSAXjk1\n8n9+M2vi5vmjZQ/yHbV+Amhra0tOTm5sbMzOzv71r3+tHbx06VJqauqVK1fy8/Pj4+O7fWJKSkpZ\nWZnFYgngsHplt9s5UZ6w2+1CCM7VfXGiPHFy5P+0/diWFOkqWpcke5ZO7qjkyJEjNptt9erVXY7/\n7W9/s9lsaWlp93rismXLbDabn6czgu3bt9tsts8//1z2IKo7deqUzWY7cuSI7EFU9/nnn9tstu3b\n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\npAgAADCp/w/UelOBKsEppQAAAABJRU5ErkJggg==\n" + }, "metadata": {}, - "outputs": [ - { - "output_type": "pyout", - "prompt_number": 6, - "text": [ - "True" - ] - } - ], - "prompt_number": 6 + "output_type": "display_data" + } + ], + "source": [ + "res_dict = matlab.run_code('figure; plot(b); b')\n", + "\n", + "from IPython.core.displaypub import publish_display_data\n", + "imgfiles = res_dict['content']['figures']\n", + "text_output = res_dict['content']['stdout']\n", + "\n", + "display_data = []\n", + "if text_output:\n", + " display_data.append({'text/plain':text_output})\n", + "for imgf in imgfiles:\n", + " try: # python 2\n", + " image = open(imgf, 'rb').read().decode('utf-8') \n", + " except: # python 3\n", + " image = open(imgf, 'rb').read()\n", + " \n", + " display_data.append({'image/png': image})\n", + "\n", + "for d in display_data:\n", + " publish_display_data(d)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "." + ] }, { - "cell_type": "code", - "collapsed": false, - "input": [ - "matlab.stop()" - ], - "language": "python", + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 6, "metadata": {}, - "outputs": [ - { - "output_type": "stream", - "stream": "stdout", - "text": [ - "MATLAB closed\n" - ] - }, - { - "output_type": "pyout", - "prompt_number": 7, - "text": [ - "True" - ] - } - ], - "prompt_number": 7 + "output_type": "execute_result" + } + ], + "source": [ + "matlab.is_connected()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MATLAB closed\n" + ] }, { - "cell_type": "code", - "collapsed": false, - "input": [ - "matlab.is_connected()" - ], - "language": "python", + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 7, "metadata": {}, - "outputs": [ - { - "output_type": "pyout", - "prompt_number": 8, - "text": [ - "False" - ] - } - ], - "prompt_number": 8 - }, + "output_type": "execute_result" + } + ], + "source": [ + "matlab.stop()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false + }, + "outputs": [ { - "cell_type": "code", - "collapsed": false, - "input": [], - "language": "python", + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 9, "metadata": {}, - "outputs": [] + "output_type": "execute_result" } ], - "metadata": {} + "source": [ + "matlab.is_connected()" + ] } - ] -} \ No newline at end of file + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.4.2" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pymatbridge/matlab/matlabserver.m b/pymatbridge/matlab/matlabserver.m index 55b223b..e70bc71 100644 --- a/pymatbridge/matlab/matlabserver.m +++ b/pymatbridge/matlab/matlabserver.m @@ -3,12 +3,14 @@ function matlabserver(socket_address) % over the socket. I then enters the listen-respond mode until it gets an % "exit" command -json.startup +json_startup messenger('init', socket_address); +c=onCleanup(@()exit); + while(1) msg_in = messenger('listen'); - req = json.load(msg_in); + req = json_load(msg_in); switch(req.cmd) case {'connect'} @@ -16,22 +18,10 @@ function matlabserver(socket_address) case {'exit'} messenger('exit'); - clear mex; break; - case {'run_function'} - fhandle = str2func('pymat_feval'); - resp = feval(fhandle, req); - messenger('respond', resp); - - case {'run_code'} - fhandle = str2func('pymat_eval'); - resp = feval(fhandle, req); - messenger('respond', resp); - - case {'get_var'} - fhandle = str2func('pymat_get_variable'); - resp = feval(fhandle, req); + case {'eval'} + resp = pymat_eval(req); messenger('respond', resp); otherwise diff --git a/pymatbridge/matlab/usrprog/demo_func.m b/pymatbridge/matlab/usrprog/demo_func.m index f0b6239..394841b 100644 --- a/pymatbridge/matlab/usrprog/demo_func.m +++ b/pymatbridge/matlab/usrprog/demo_func.m @@ -1,5 +1,4 @@ -function res = test(args) -% demonstration function; formerly known as 'test.m' +function res = demo_func(args) res = args.a + 1; figure diff --git a/pymatbridge/matlab/usrprog/precision_divide.m b/pymatbridge/matlab/usrprog/precision_divide.m index 6948cc9..0e16d5c 100644 --- a/pymatbridge/matlab/usrprog/precision_divide.m +++ b/pymatbridge/matlab/usrprog/precision_divide.m @@ -1,4 +1,4 @@ -function res = test_precision_divide(args) +function res = precision_divide(args) % This function devides val1 by val2 and returns the result res = args.val1 / args.val2; diff --git a/pymatbridge/matlab/usrprog/precision_multiply.m b/pymatbridge/matlab/usrprog/precision_multiply.m index 11557f5..1c12f23 100644 --- a/pymatbridge/matlab/usrprog/precision_multiply.m +++ b/pymatbridge/matlab/usrprog/precision_multiply.m @@ -1,4 +1,4 @@ -function res = test_precision_multiply(args) +function res = precision_multiply(args) % This function returns the product of two arguments val1 and val2 res = args.val1 * args.val2; diff --git a/pymatbridge/matlab/usrprog/precision_pass.m b/pymatbridge/matlab/usrprog/precision_pass.m index afc57bd..1225c1c 100644 --- a/pymatbridge/matlab/usrprog/precision_pass.m +++ b/pymatbridge/matlab/usrprog/precision_pass.m @@ -1,4 +1,4 @@ -function res = test_precision_pass(args) +function res = precision_pass(args) % This function takes an argument val and simply return it res = args.val; diff --git a/pymatbridge/matlab/usrprog/precision_sqrt.m b/pymatbridge/matlab/usrprog/precision_sqrt.m index c6a4e35..ef87cf0 100644 --- a/pymatbridge/matlab/usrprog/precision_sqrt.m +++ b/pymatbridge/matlab/usrprog/precision_sqrt.m @@ -1,4 +1,4 @@ -function res = test_precision_sqrt(args) +function res = precision_sqrt(args) % This function returns the square root of the value res = sqrt(args.val); diff --git a/pymatbridge/matlab/usrprog/precision_sum.m b/pymatbridge/matlab/usrprog/precision_sum.m index ce0528c..6d6bd45 100644 --- a/pymatbridge/matlab/usrprog/precision_sum.m +++ b/pymatbridge/matlab/usrprog/precision_sum.m @@ -1,4 +1,4 @@ -function res = test_precision_sum(args) +function res = precision_sum(args) % This function returns the sum of two arguments val1 and val2 res = args.val1 + args.val2; diff --git a/pymatbridge/matlab/util/isrow.m b/pymatbridge/matlab/util/isrow.m new file mode 100644 index 0000000..12848f7 --- /dev/null +++ b/pymatbridge/matlab/util/isrow.m @@ -0,0 +1,14 @@ +function Y = isrow(X) +% +% ISROW True for row vectors. +% +% Y = ISROW(X) returns logical 1 if X is a row vector, 0 otherwise. +% ISROW returns 1 for scalars also. +% +% + +if ndims(X)==2 && size(X,1)==1 && size(X,2)>=1 + Y = logical(1); +else + Y = logical(0); +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/dump.m b/pymatbridge/matlab/util/json_v0.2.2/+json/dump.m deleted file mode 100644 index 00690a0..0000000 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/dump.m +++ /dev/null @@ -1,152 +0,0 @@ -function str = dump(value, varargin) -%DUMP Encode matlab value into a JSON string. -% -% SYNOPSIS -% -% str = json.dump(value) -% str = json.dump(..., optionName, optionValue, ...) -% -% The function converts a matlab value to a JSON string. A value can be any -% of a double array, a logical array, a char array, a cell array, or a -% struct array. Numeric values other than double are converted to double. -% A struct array is mapped to a JSON object. However, since a JSON object -% is unordered, the order of field names are not preserved. -% -% OPTIONS -% -% The function takes following options. -% -% 'ColMajor' Represent matrix in column-major order. Default false. -% 'indent' Pretty-print the output string with indentation. Default -% [] -% -% EXAMPLE -% -% >> X = struct('matrix', magic(2), 'char', 'hello'); -% >> str = json.dump(X) -% str = -% -% {"char":"hello","matrix":[[1,3],[4,2]]} -% -% >> str = json.dump([1,2,3;4,5,6]) -% str = -% -% [[1,2,3],[4,5,6]] -% -% >> str = json.dump([1,2,3;4,5,6], 'ColMajor', true) -% str = -% -% [[1,4],[2,5],[3,6]] -% -% >> str = json.dump([1,2,3;4,5,6], 'indent', 2) -% str = -% -% [ -% [ -% 1, -% 2, -% 3 -% ], -% [ -% 4, -% 5, -% 6 -% ] -% ] -% -% NOTE -% -% Since any matlab values are an array, it is impossible to uniquely map -% all matlab values to JSON primitives. This implementation aims to have -% better interoperability across platforms. Therefore, some matlab values -% are mapped to the same representation. For example, [1,2] and {1,2} are -% mapped to the same json string '[1,2]'. -% -% See also json.load json.write - - json.startup('WarnOnAddpath', true); - options = get_options_(varargin{:}); - obj = dump_data_(value, options); - if isempty(options.indent) - str = char(obj.toString()); - else - str = char(obj.toString(options.indent)); - end -end - -function options = get_options_(varargin) -%GET_OPTIONS_ - options = struct(... - 'ColMajor', false,... - 'indent', [] ... - ); - for i = 1:2:numel(varargin) - switch varargin{i} - case 'ColMajor' - options.ColMajor = logical(varargin{i+1}); - case 'indent' - options.indent = varargin{i+1}; - otherwise - error('Unknown option to json.dump') - end - end -end - -function obj = dump_data_(value, options) -%DUMP_DATA_ - if ischar(value) && (isvector(value) || isempty(value)) - obj = java.lang.String(value); - elseif isempty(value) && isnumeric(value) - obj = org.json.JSONObject.NULL; - elseif ~isscalar(value) - obj = org.json.JSONArray(); - - if ndims(value) > 2 - split_value = num2cell(value, 1:ndims(value)-1); - for i = 1:numel(split_value) - obj.put(dump_data_(split_value{i}, options)); - end - else - if options.ColMajor && iscolumn(value) || ... - ~options.ColMajor && isrow(value) - if iscell(value) - for i = 1:numel(value), obj.put(dump_data_(value{i}, options)); end - else - for i = 1:numel(value), obj.put(dump_data_(value(i), options)); end - end - else - value = num2cell(value, 2 - options.ColMajor); - if all(cellfun(@isscalar, value)) - for i = 1:numel(value), obj.put(dump_data_(value(i), options)); end - else - for i = 1:numel(value), obj.put(dump_data_(value{i}, options)); end - end - end - end - elseif iscell(value) - obj = org.json.JSONArray(); - for i = 1:numel(value) - obj.put(dump_data_(value{i}, options)); - end - elseif isnumeric(value) - if isreal(value) - obj = java.lang.Double(value); - % Encode complex number as a struct - else - complex_struct = struct; - complex_struct.real = real(value); - complex_struct.imag = imag(value); - obj = dump_data_(complex_struct, options); - end - elseif islogical(value) - obj = java.lang.Boolean(value); - elseif isstruct(value) - obj = org.json.JSONObject(); - keys = fieldnames(value); - for i = 1:length(keys) - obj.put(keys{i},dump_data_(value.(keys{i}), options)); - end - else - error('json:typeError', 'Unsupported data type: %s', class(value)); - end -end \ No newline at end of file diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/java/README b/pymatbridge/matlab/util/json_v0.2.2/json/java/README similarity index 100% rename from pymatbridge/matlab/util/json_v0.2.2/+json/java/README rename to pymatbridge/matlab/util/json_v0.2.2/json/java/README diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/java/json.jar b/pymatbridge/matlab/util/json_v0.2.2/json/java/json.jar similarity index 100% rename from pymatbridge/matlab/util/json_v0.2.2/+json/java/json.jar rename to pymatbridge/matlab/util/json_v0.2.2/json/java/json.jar 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 new file mode 100644 index 0000000..c085226 --- /dev/null +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m @@ -0,0 +1,331 @@ +function str = json_dump(value, varargin) +%DUMP Encode matlab value into a JSON string. +% +% SYNOPSIS +% +% str = json.dump(value) +% str = json.dump(..., optionName, optionValue, ...) +% +% The function converts a matlab value to a JSON string. A value can be any +% of a double array, a logical array, a char array, a cell array, or a +% struct array. Numeric values other than double are converted to double. +% A struct array is mapped to a JSON object. However, since a JSON object +% is unordered, the order of field names are not preserved. +% +% OPTIONS +% +% The function takes following options. +% +% 'ColMajor' Represent matrix in column-major order. Default false. +% 'indent' Pretty-print the output string with indentation. Default +% [] +% +% EXAMPLE +% +% >> X = struct('matrix', magic(2), 'char', 'hello'); +% >> str = json.dump(X) +% str = +% +% {"char":"hello","matrix":[[1,3],[4,2]]} +% +% >> str = json.dump([1,2,3;4,5,6]) +% str = +% +% [[1,2,3],[4,5,6]] +% +% >> str = json.dump([1,2,3;4,5,6], 'ColMajor', true) +% str = +% +% [[1,4],[2,5],[3,6]] +% +% >> str = json.dump([1,2,3;4,5,6], 'indent', 2) +% str = +% +% [ +% [ +% 1, +% 2, +% 3 +% ], +% [ +% 4, +% 5, +% 6 +% ] +% ] +% +% NOTE +% +% Since any matlab values are an array, it is impossible to uniquely map +% all matlab values to JSON primitives. This implementation aims to have +% better interoperability across platforms. Therefore, some matlab values +% are mapped to the same representation. For example, [1,2] and {1,2} are +% mapped to the same json string '[1,2]'. +% +% See also json.load json.write + json_startup('WarnOnAddpath', true); + options = get_options_(varargin{:}); + obj = dump_data_(value, options); + if isempty(options.indent) + str = char(obj.toString()); + else + str = char(obj.toString(options.indent)); + end +end + +function options = get_options_(varargin) +%GET_OPTIONS_ + options = struct(... + 'ColMajor', false,... + 'indent', [] ... + ); + for i = 1:2:numel(varargin) + switch varargin{i} + case 'ColMajor' + options.ColMajor = logical(varargin{i+1}); + case 'indent' + options.indent = varargin{i+1}; + otherwise + error('Unknown option to json.dump') + end + end +end + +function obj = dump_data_(value, options) +%DUMP_DATA_ + if ischar(value) && (isvector(value) || isempty(value)) + obj = javaObject('java.lang.String', value); + elseif isempty(value) && isnumeric(value) + json_object = javaObject('org.json.JSONObject'); + obj = json_object.NULL; + elseif ~isscalar(value) + obj = javaObject('org.json.JSONArray'); + + if isnumeric(value) + % encode arrays as a struct + double_struct = struct; + double_struct.ndarray = 1; + value = double(value); + if isreal(value) + double_struct.data = base64encode(typecast(value(:), 'uint8')); + else + double_struct.real = base64encode(typecast(real(value(:)), 'uint8')); + double_struct.imag = base64encode(typecast(imag(value(:)), 'uint8')); + end + double_struct.shape = base64encode(typecast(size(value), 'uint8')); + obj = dump_data_(double_struct, options); + elseif ndims(value) > 2 + split_value = num2cell(value, 1:ndims(value)-1); + for i = 1:numel(split_value) + obj.put(dump_data_(split_value{i}, options)); + end + else + if options.ColMajor && iscolumn(value) || ... + ~options.ColMajor && isrow(value) + if iscell(value) + for i = 1:numel(value), obj.put(dump_data_(value{i}, options)); end + else + for i = 1:numel(value), obj.put(dump_data_(value(i), options)); end + end + else + value = num2cell(value, 2 - options.ColMajor); + if all(cellfun(@isscalar, value)) + for i = 1:numel(value), obj.put(dump_data_(value(i), options)); end + else + for i = 1:numel(value), obj.put(dump_data_(value{i}, options)); end + end + end + end + elseif iscell(value) + obj = javaObject('org.json.JSONArray'); + for i = 1:numel(value) + obj.put(dump_data_(value{i}, options)); + end + elseif isnumeric(value) + if isreal(value) + obj = value; + % Encode complex number as a struct + else + complex_struct = struct; + complex_struct.real = real(value); + complex_struct.imag = imag(value); + obj = dump_data_(complex_struct, options); + end + elseif islogical(value) + obj = javaObject('java.lang.Boolean', value); + elseif isstruct(value) + obj = javaObject('org.json.JSONObject'); + keys = fieldnames(value); + for i = 1:length(keys) + 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)); + end +end + + +function y = base64encode(x, eol) +%BASE64ENCODE Perform base64 encoding on a string. +% +% BASE64ENCODE(STR, EOL) encode the given string STR. EOL is the line ending +% sequence to use; it is optional and defaults to '\n' (ASCII decimal 10). +% The returned encoded string is broken into lines of no more than 76 +% characters each, and each line will end with EOL unless it is empty. Let +% EOL be empty if you do not want the encoded string broken into lines. +% +% STR and EOL don't have to be strings (i.e., char arrays). The only +% requirement is that they are vectors containing values in the range 0-255. +% +% This function may be used to encode strings into the Base64 encoding +% specified in RFC 2045 - MIME (Multipurpose Internet Mail Extensions). The +% Base64 encoding is designed to represent arbitrary sequences of octets in a +% form that need not be humanly readable. A 65-character subset +% ([A-Za-z0-9+/=]) of US-ASCII is used, enabling 6 bits to be represented per +% printable character. +% +% Examples +% -------- +% +% If you want to encode a large file, you should encode it in chunks that are +% a multiple of 57 bytes. This ensures that the base64 lines line up and +% that you do not end up with padding in the middle. 57 bytes of data fills +% one complete base64 line (76 == 57*4/3): +% +% If ifid and ofid are two file identifiers opened for reading and writing, +% respectively, then you can base64 encode the data with +% +% while ~feof(ifid) +% fwrite(ofid, base64encode(fread(ifid, 60*57))); +% end +% +% or, if you have enough memory, +% +% fwrite(ofid, base64encode(fread(ifid))); +% +% See also BASE64DECODE. + +% Author: Peter J. Acklam +% Time-stamp: 2004-02-03 21:36:56 +0100 +% E-mail: pjacklam@online.no +% URL: http://home.online.no/~pjacklam + + % check number of input arguments + error(nargchk(1, 2, nargin)); + + % make sure we have the EOL value + if nargin < 2 + eol = ''; %sprintf('\n'); + else + if sum(size(eol) > 1) > 1 + error('EOL must be a vector.'); + end + if any(eol(:) > 255) + error('EOL can not contain values larger than 255.'); + end + end + + if sum(size(x) > 1) > 1 + error('STR must be a vector.'); + end + + x = uint8(x); + eol = uint8(eol); + + ndbytes = length(x); % number of decoded bytes + nchunks = ceil(ndbytes / 3); % number of chunks/groups + nebytes = 4 * nchunks; % number of encoded bytes + + % add padding if necessary, to make the length of x a multiple of 3 + if rem(ndbytes, 3) + x(end+1 : 3*nchunks) = 0; + end + + x = reshape(x, [3, nchunks]); % reshape the data + y = repmat(uint8(0), 4, nchunks); % for the encoded data + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % Split up every 3 bytes into 4 pieces + % + % aaaaaabb bbbbcccc ccdddddd + % + % to form + % + % 00aaaaaa 00bbbbbb 00cccccc 00dddddd + % + y(1,:) = bitshift(x(1,:), -2); % 6 highest bits of x(1,:) + + y(2,:) = bitshift(bitand(x(1,:), 3), 4); % 2 lowest bits of x(1,:) + y(2,:) = bitor(y(2,:), bitshift(x(2,:), -4)); % 4 highest bits of x(2,:) + + y(3,:) = bitshift(bitand(x(2,:), 15), 2); % 4 lowest bits of x(2,:) + y(3,:) = bitor(y(3,:), bitshift(x(3,:), -6)); % 2 highest bits of x(3,:) + + y(4,:) = bitand(x(3,:), 63); % 6 lowest bits of x(3,:) + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % Now perform the following mapping + % + % 0 - 25 -> A-Z + % 26 - 51 -> a-z + % 52 - 61 -> 0-9 + % 62 -> + + % 63 -> / + % + % We could use a mapping vector like + % + % ['A':'Z', 'a':'z', '0':'9', '+/'] + % + % but that would require an index vector of class double. + % + z = repmat(uint8(0), size(y)); + i = y <= 25; z(i) = 'A' + double(y(i)); + i = 26 <= y & y <= 51; z(i) = 'a' - 26 + double(y(i)); + i = 52 <= y & y <= 61; z(i) = '0' - 52 + double(y(i)); + i = y == 62; z(i) = '+'; + i = y == 63; z(i) = '/'; + y = z; + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % Add padding if necessary. + % + npbytes = 3 * nchunks - ndbytes; % number of padding bytes + if npbytes + y(end-npbytes+1 : end) = '='; % '=' is used for padding + end + + if isempty(eol) + + % reshape to a row vector + y = reshape(y, [1, nebytes]); + + else + + nlines = ceil(nebytes / 76); % number of lines + neolbytes = length(eol); % number of bytes in eol string + + % pad data so it becomes a multiple of 76 elements + y(nebytes + 1 : 76 * nlines) = 0; + y = reshape(y, 76, nlines); + + % insert eol strings + eol = eol(:); + y(end + 1 : end + neolbytes, :) = eol(:, ones(1, nlines)); + + % remove padding, but keep the last eol string + m = nebytes + neolbytes * (nlines - 1); + n = (76+neolbytes)*nlines - neolbytes; + y(m+1 : n) = ''; + + % extract and reshape to row vector + y = reshape(y, 1, m+neolbytes); + + end + + % output is a character array + y = char(y); +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/load.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m similarity index 54% rename from pymatbridge/matlab/util/json_v0.2.2/+json/load.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_load.m index 3829a79..ed2660f 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/load.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m @@ -1,4 +1,4 @@ -function value = load(str, varargin) +function value = json_load(str, varargin) %LOAD Load matlab value from a JSON string. % % SYNOPSIS @@ -57,7 +57,7 @@ % % See also json.dump json.read - json.startup('WarnOnAddpath', true); + json_startup('WarnOnAddpath', true); options = get_options_(varargin{:}); singleton = false; @@ -66,11 +66,11 @@ error('json:invalidString','Invalid JSON string'); end if str(1)=='{' - node = org.json.JSONObject(java.lang.String(str)); + node = javaObject('org.json.JSONObject', javaObject('java.lang.String', str)); else singleton = str(1) ~= '[' && str(end) ~= ']'; if singleton, str = ['[',str,']']; end - node = org.json.JSONArray(java.lang.String(str)); + node = javaObject('org.json.JSONArray', javaObject('java.lang.String', str)); end value = parse_data_(node, options); if singleton, value = value{:}; end @@ -119,15 +119,25 @@ warning('json:fieldNameConflict', ... 'Field %s renamed to %s', field, safe_field); end - value.(safe_field) = parse_data_(node.get(java.lang.String(key)), ... + value.(safe_field) = parse_data_(node.get(javaObject('java.lang.String', key)), ... options); end - % Check if the struct just decoded represents a complex number - if isfield(value,'real') && isfield(value, 'imag') + % Check if the struct just decoded represents an array or complex number + if isfield(value,'ndarray') && isfield(value, 'shape') + if isfield(value, 'data') + arr = typecast(base64decode(value.data), 'double'); + else + r = typecast(base64decode(value.real), 'double'); + im = typecast(base64decode(value.imag), 'double'); + arr = complex(r, im); + end + value = reshape(arr, value.shape); + elseif isfield(value,'real') && isfield(value, 'imag') complex_value = complex(value.real, value.imag); value = complex_value; end - elseif isa(node, 'org.json.JSONObject$Null') + % In MATLAB, nested classes end up with a $ in the name, in Octave it's a . + elseif isa(node, 'org.json.JSONObject$Null') || isa(node, 'org.json.JSONObject.Null') value = []; else error('json:typeError', 'Unknown data type: %s', class(node)); @@ -196,4 +206,93 @@ fields = fieldnames(value); vec = [vec, uint8([fields{:}])]; end -end \ No newline at end of file +end + + + +function y = base64decode(x) + %BASE64DECODE Perform base64 decoding on a string. + % + % BASE64DECODE(STR) decodes the given base64 string STR. + % + % Any character not part of the 65-character base64 subset set is silently + % ignored. + % + % This function is used to decode strings from the Base64 encoding specified + % in RFC 2045 - MIME (Multipurpose Internet Mail Extensions). The Base64 + % encoding is designed to represent arbitrary sequences of octets in a form + % that need not be humanly readable. A 65-character subset ([A-Za-z0-9+/=]) + % of US-ASCII is used, enabling 6 bits to be represented per printable + % character. + % + % See also BASE64ENCODE. + + % Author: Peter J. Acklam + % Time-stamp: 2004-09-20 08:20:50 +0200 + % E-mail: pjacklam@online.no + % URL: http://home.online.no/~pjacklam + + % Modified by Guillaume Flandin, May 2008 + + % check number of input arguments + %-------------------------------------------------------------------------- + + error(nargchk(1, 1, nargin)); + + % Perform the following mapping + %-------------------------------------------------------------------------- + % A-Z -> 0 - 25 a-z -> 26 - 51 0-9 -> 52 - 61 + % + -> 62 / -> 63 = -> 64 + % anything else -> NaN + + base64chars = NaN(1,256); + base64chars('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=') = 0:64; + x = base64chars(x); + + % Remove/ignore any characters not in the base64 characters list or '=' + %-------------------------------------------------------------------------- + + x = x(~isnan(x)); + + % Replace any incoming padding ('=' -> 64) with a zero pad + %-------------------------------------------------------------------------- + + if x(end-1) == 64, p = 2; x(end-1:end) = 0; + elseif x(end) == 64, p = 1; x(end) = 0; + else p = 0; + end + + % Allocate decoded data array + %-------------------------------------------------------------------------- + + n = length(x) / 4; % number of groups + x = reshape(uint8(x), 4, n); % input data + y = zeros(3, n, 'uint8'); % decoded data + + % Rearrange every 4 bytes into 3 bytes + %-------------------------------------------------------------------------- + % 00aaaaaa 00bbbbbb 00cccccc 00dddddd + % + % to form + % + % aaaaaabb bbbbcccc ccdddddd + + y(1,:) = bitshift(x(1,:), 2); % 6 highest bits of y(1,:) + y(1,:) = bitor(y(1,:), bitshift(x(2,:), -4)); % 2 lowest bits of y(1,:) + + y(2,:) = bitshift(x(2,:), 4); % 4 highest bits of y(2,:) + y(2,:) = bitor(y(2,:), bitshift(x(3,:), -2)); % 4 lowest bits of y(2,:) + + y(3,:) = bitshift(x(3,:), 6); % 2 highest bits of y(3,:) + y(3,:) = bitor(y(3,:), x(4,:)); % 6 lowest bits of y(3,:) + + % Remove any zero pad that was added to make this a multiple of 24 bits + %-------------------------------------------------------------------------- + + if p, y(end-p+1:end) = []; end + + % Reshape to a row vector + %-------------------------------------------------------------------------- + + y = reshape(y, 1, []); +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/read.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_read.m similarity index 88% rename from pymatbridge/matlab/util/json_v0.2.2/+json/read.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_read.m index 411bc6e..213fa47 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/read.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_read.m @@ -1,4 +1,4 @@ -function value = read(filename, varargin) +function value = json_read(filename, varargin) %READ Load a matlab value from a JSON file. % % SYNOPSIS @@ -19,7 +19,7 @@ fid = 0; try fid = fopen(filename, 'r'); - value = json.load(fscanf(fid, '%c', inf)); + value = json_load(fscanf(fid, '%c', inf)); fclose(fid); catch e if fid ~= 0, fclose(fid); end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/startup.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_startup.m similarity index 97% rename from pymatbridge/matlab/util/json_v0.2.2/+json/startup.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_startup.m index f88edac..77c0853 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/startup.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_startup.m @@ -1,4 +1,4 @@ -function startup(varargin) +function json_startup(varargin) %STARTUP Initialize runtime environment. % % SYNOPSIS @@ -42,4 +42,4 @@ function startup(varargin) ]); end end -end \ No newline at end of file +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/write.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_write.m similarity index 87% rename from pymatbridge/matlab/util/json_v0.2.2/+json/write.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_write.m index 707dd08..8d8862c 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/write.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_write.m @@ -1,4 +1,4 @@ -function write(value, filename, varargin) +function json_write(value, filename, varargin) %WRITE Write a matlab value into a JSON file. % % SYNOPSIS @@ -19,7 +19,7 @@ function write(value, filename, varargin) fid = 0; try fid = fopen(filename, 'w'); - fprintf(fid, '%s', json.dump(value, varargin{:})); + fprintf(fid, '%s', json_dump(value, varargin{:})); fclose(fid); catch e if fid ~= 0, fclose(fid); end diff --git a/pymatbridge/matlab/util/make_figs.m b/pymatbridge/matlab/util/make_figs.m index 239110e..94c32ba 100644 --- a/pymatbridge/matlab/util/make_figs.m +++ b/pymatbridge/matlab/util/make_figs.m @@ -14,7 +14,9 @@ saveas(h, [filename, '.png']); % Once you've saved it, close it, so it doesn't get dragged into the % scope of other cells - close(h); + if (strcmp(get(h, 'visible'), 'off')) + close(h); + end fig_files{fig} = [filename '.png']; end diff --git a/pymatbridge/matlab/util/publish_notebook.m b/pymatbridge/matlab/util/publish_notebook.m new file mode 100644 index 0000000..85657cb --- /dev/null +++ b/pymatbridge/matlab/util/publish_notebook.m @@ -0,0 +1,38 @@ +function publish_notebook(mfile, varargin) +% function publish_notebook(mfile, [outputfile]) +% +% Publish a Matlab m file as an interactive notebook in .ipynb format +% +% Parameters +% ---------- +% mfile : str +% Full path to the input m file. +% outputfile +% Full path to the output .ipynb file + +if length(varargin) + outputfile = sprintf('--outfile %s', varargin{1}); +else + outputfile = ''; +end + +% Matlab system path-setting is broken, so we place the +% publish-notebook executable right here, so we can easily find it +libpath = fileparts(which(mfilename)); +prefix = strsplit(libpath, 'lib'); +binpath = strcat(prefix(1), 'bin'); +cmd_str = sprintf('publish-notebook %s %s', mfile, outputfile); + +setenv('PATH', sprintf('%s:%s', binpath{1}, getenv('PATH'))); +[status, result] = system(cmd_str); + +if status == 0 + disp('Conversion completed'); +else + disp(sprintf('There was a problem converting the file %s:', mfile)); + disp(result) +end + + + + diff --git a/pymatbridge/matlab/util/pymat_eval.m b/pymatbridge/matlab/util/pymat_eval.m index b071cb8..a564be1 100644 --- a/pymatbridge/matlab/util/pymat_eval.m +++ b/pymatbridge/matlab/util/pymat_eval.m @@ -1,42 +1,54 @@ -function json_response = web_eval(req); -%WEB_EVAL: Returns a json object of the result of calling the function +function json_response = pymat_eval(req); +% PYMAT_EVAL: Returns a json object of the result of calling the function % -% json_response = WEB_EVAL(headers); -% json_response = WEB_EVAL(headers, config); +% json_response = pymat_eval(req); % -% This allows you to run any matlab code. To be used with webserver.m. -% HTTP POST to /web_eval.m with the following parameters: -% code: a string which contains the code to be run in the matlab session +% This allows you to run any matlab code. req should be a struct with the +% following fields: +% dname: The name of a directory to add to the runtime path before attempting to run the code. +% func_name: The name of a function to invoke. +% func_args: An array of arguments to send to the function. +% nargout: An int specifying how many output arguments are expected. % -% Should return a json object containing the result +% Should return a json object containing the result. % % Based on Max Jaderberg's web_feval -response.success = 'false'; -field_names = fieldnames(req); - +response.success = true; response.content = ''; +response.result = ''; +response.stack = {}; -code_check = false; -if size(field_names) - if isfield(req, 'code') - code_check = true; - end -end - -if ~code_check - response.message = 'No code provided as POST parameter'; - json_response = json.dump(response); - return; -end - -code = req.code; +close all hidden; try % tempname is less likely to get bonked by another process. diary_file = [tempname() '_diary.txt']; diary(diary_file); - evalin('base', code); + + % Add function path to current path + if req.dname + addpath(req.dname); + end + + % force a rehash of user functions + rehash + + if iscell(req.func_args) + func_args = req.func_args; + else + % If we don't have a cell, the JSON decoder has managed to merge + % everything into an array, which we don't want + func_args = num2cell(req.func_args, 1); + end + [resp{1:req.nargout}] = feval(req.func_name, func_args{:}); + + if req.nargout == 1 + response.result = resp{1}; + else + response.result = resp; + end + diary('off'); datadir = fullfile(tempdir(),'MatlabData'); @@ -46,8 +58,6 @@ end fig_files = make_figs(datadir); - - response.success = 'true'; response.content.figures = fig_files; % this will not work on Windows: @@ -59,18 +69,61 @@ fclose(FID); response.content.stdout = stdout; else - response.success = 'false'; + response.success = false; response.content.stdout = sprintf('could not open %s for read',diary_file); end delete(diary_file) catch ME diary('off'); - response.success = 'false'; + response.success = false; response.content.stdout = ME.message; -end -response.content.code = code; + % Retrieve the stack trace. + if ~exist('OCTAVE_VERSION', 'builtin'); + % For MATLAB, just grab it off the exception. + response.stack = ME.stack; + else + % Octave exceptions don't seem to have a 'stack' field, so we use lasterror + % instead. lasterror exists in MATLAB too, but it doesn't seem to return + % the correct stack trace in this case. + err = lasterror(); + stack = err.stack; + + % Strip off fields that aren't available on MException. + stack = rmfield(stack, {'column', 'context', 'scope'}); + + % The 'name' fields here look like 'foo>bar' in Octave, where foo is the + % outermost calling function, and bar is the current function. With + % MException there's only 'bar' so let's strip off the 'foo>'. + + % n.b. strsplit doesn't work in Octave when the delimiter is < or >. + % This bug is fixed in Octave 4.0 (http://savannah.gnu.org/bugs/?44641) + % but we support 3.8, so we use the regex mode as a workaround. + for i = 1:numel(stack) + name = stack(i).name; + name = strsplit(name, '>', 'delimitertype', 'regularexpression'); + stack(i).name = name{end}; + end + + response.stack = stack; + end + + % Strip off the last two frames -- these correspond to pymat_eval and + % matlabserver, which we don't want to expose. + response.stack = response.stack(1:end-2, :); + + % FIXME: The stack is returned as a nx1 struct array, and in Octave json_dump + % throws an error trying to serialize it. Let's just transpose it for now. + response.stack = response.stack'; + % FIXME: json_dump loops infinitely if this is a 1x0 struct array, so + % let's just turn it back into an empty array if so. + if ndims(response.stack) == 2 && ... + size(response.stack, 1) == 1 && ... + size(response.stack, 2) == 0 + response.stack = {}; + end +end -json_response = json.dump(response); +json_response = json_dump(response); end %function diff --git a/pymatbridge/matlab/util/pymat_feval.m b/pymatbridge/matlab/util/pymat_feval.m deleted file mode 100644 index 2fd3a48..0000000 --- a/pymatbridge/matlab/util/pymat_feval.m +++ /dev/null @@ -1,42 +0,0 @@ -% Max Jaderberg 2011 - -function json_response = matlab_feval(req) - - response.success = 'false'; - field_names = fieldnames(req); - - response.result = ''; - - func_path_check = false; - arguments_check = false; - if size(field_names) - if isfield(req, 'func_path') - func_path_check = true; - end - if isfield(req, 'func_args') - arguments_check = true; - end - end - - if ~func_path_check - response.message = 'No function given as func_path POST parameter'; - json_response = json.dump(response); - return - end - - func_path = req.func_path; - if arguments_check - arguments = req.func_args; - else - arguments = ''; - end - - response.result = run_dot_m(func_path, arguments); - response.success = 'true'; - response.message = 'Successfully completed request'; - - json_response = json.dump(response); - - return - -end diff --git a/pymatbridge/matlab/util/pymat_get_variable.m b/pymatbridge/matlab/util/pymat_get_variable.m deleted file mode 100644 index c37fa90..0000000 --- a/pymatbridge/matlab/util/pymat_get_variable.m +++ /dev/null @@ -1,32 +0,0 @@ -function json_response = pymat_get_variable(req) -% Reach into the current namespace get a variable in json format that can -% be returned as part of a response - -response.success = 'false'; - -field_names = fieldnames(req); - -response.content = ''; - -varname_check = false; -if size(field_names) - if isfield(req, 'varname') - varname_check = true; - end -end - -if ~varname_check - response.message = 'No variable name provided as input argument'; - json_response = json.dump(response); - return -end - - -varname = req.varname; - -response.var = evalin('base', varname); - -json_response = json.dump(response); - -return -end diff --git a/pymatbridge/matlab/util/pymat_set_variable.m b/pymatbridge/matlab/util/pymat_set_variable.m deleted file mode 100644 index e3936f4..0000000 --- a/pymatbridge/matlab/util/pymat_set_variable.m +++ /dev/null @@ -1,7 +0,0 @@ -function res = pymat_set_variable(args) -% Setup a variable in Matlab workspace - - assignin('base', args.name, args.value); - res = 1; - -end %function diff --git a/pymatbridge/matlab/util/run_dot_m.m b/pymatbridge/matlab/util/run_dot_m.m deleted file mode 100644 index 7f78970..0000000 --- a/pymatbridge/matlab/util/run_dot_m.m +++ /dev/null @@ -1,28 +0,0 @@ -% Max Jaderberg 2011 - -function result = run_dot_m( file_to_run, arguments ) -%RUN_DOT_M Runs the given .m file with the argument struct given -% For exmaple run_dot_m('/path/to/function.m', args); -% args is a struct containing the arguments. function.m must take only -% one parameter, the argument structure - - [dir, func_name, ext] = fileparts(file_to_run); - - if ~size(ext) - result = 'Error: Need to give path to .m file'; - return - end - - if ~strcmp(ext, '.m') - result = 'Error: Need to give path to .m file'; - return - end - -% Add function path to current path - addpath(dir); - if isstruct(arguments) - result = feval(func_name, arguments); - else - result = feval(func_name); - end -end diff --git a/pymatbridge/matlab_magic.py b/pymatbridge/matlab_magic.py index c019ad7..fb2c4df 100644 --- a/pymatbridge/matlab_magic.py +++ b/pymatbridge/matlab_magic.py @@ -5,38 +5,22 @@ Magic command interface for interactive work with Matlab(R) via the pymatbridge - -Note -==== -Thanks to Max Jaderberg for his work on pymatbridge. - """ -import sys, os -import tempfile -from glob import glob from shutil import rmtree -from getopt import getopt -from urllib2 import URLError 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 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 class MatlabInterperterError(RuntimeError): @@ -56,7 +40,7 @@ def __unicode__(self): __str__ = __unicode__ else: def __str__(self): - return unicode_to_str(unicode(self), 'utf-8') + return unicode_to_str(text_type(self), 'utf-8') @@ -67,9 +51,8 @@ class MatlabMagics(Magics): """ def __init__(self, shell, matlab='matlab', - maxtime=10, pyconverter=np.asarray, - cache_display_data=False): + **kwargs): """ Parameters ---------- @@ -80,43 +63,29 @@ def __init__(self, shell, The system call to start a matlab session. Allows you to choose a particular version of matlab if you want - maxtime : float - The maximal time to wait for responses for matlab (in seconds). - Default: 10 seconds. - pyconverter : callable To be called on matlab variables returning into the ipython namespace - cache_display_data : bool - If True, the published results of the final call to R are - cached in the variable 'display_cache'. - + kwargs: additional key-word arguments to pass to initialization of + the Matlab/Octave process """ 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, **kwargs) + else: + self.Matlab = pymat.Matlab(matlab, **kwargs) self.Matlab.start() self.pyconverter = pyconverter - def __del__(self): - """shut down the Matlab server when the object dies. - - 2FIX: this seems to not be called when ipython terminates. bleah. - """ - try: - self.Matlab.stop() - except: - raise - def eval(self, line): """ Parse and evaluate a single line of matlab """ - run_dict = self.Matlab.run_code(line, maxtime=self.Matlab.maxtime) + run_dict = self.Matlab.run_code(line) - if run_dict['success'] == 'false': + if not run_dict['success']: raise MatlabInterperterError(line, run_dict['content']['stdout']) # This is the matlab stdout: @@ -126,11 +95,9 @@ def set_matlab_var(self, name, value): """ Set up a variable in Matlab workspace """ - run_dict = self.Matlab.run_func("pymat_set_variable.m", - {'name':name, 'value':value}, - maxtime=self.Matlab.maxtime) + run_dict = self.Matlab.set_variable(name, value) - if run_dict['success'] == 'false': + if not run_dict['success']: raise MatlabInterperterError(line, run_dict['content']['stdout']) @@ -145,6 +112,21 @@ def set_matlab_var(self, name, value): help='Names of variables to be pushed from matlab to shell.user_ns after executing cell body and applying self.Matlab.get_variable(). Multiple names can be passed separated only by commas with no whitespace.' ) + @argument( + '-s', '--silent', action='store_true', + help='Do not display text output of MATLAB command' + ) + + @argument( + '-S', '--size', action='store', default='512,384', + help='Pixel size of plots, "width,height.' + ) + + @argument( + '-g', '--gui', action='store_true', + help='Show plots in a graphical user interface' + ) + @argument( 'code', nargs='*', @@ -154,96 +136,69 @@ def set_matlab_var(self, name, value): @needs_local_scope @line_cell_magic def matlab(self, line, cell=None, local_ns=None): - """ - - Execute code in matlab + "Execute code in matlab." - """ args = parse_argstring(self.matlab, line) - - # arguments 'code' in line are prepended to - # the cell lines - - if cell is None: - code = '' - return_output = True - line_mode = True - else: - code = cell - return_output = False - line_mode = False - - code = ' '.join(args.code) + code + code = line if cell is None else ' '.join(args.code) + cell if local_ns is None: local_ns = {} + width, height = args.size.split(',') + self.Matlab.set_plot_settings(width, height, not args.gui) 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] - - # To make an array JSON serializable - if (isinstance(val, np.ndarray)): - val = val.tolist() - - self.set_matlab_var(input, val) - - else: - raise RuntimeError(no_io_str) - - text_output = '' - #imgfiles = [] + 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: - if line_mode: - e_s = "There was an error running the code:\n %s"%line - result_dict = self.eval(line) - else: - e_s = "There was an error running the code:\n %s"%code - result_dict = self.eval(code) - except URLError: - e_s += "\n-----------------------" - e_s += "\nAre you sure Matlab is started?" - raise RuntimeError(e_s) - - - - text_output += result_dict['content']['stdout'] + result_dict = self.eval(code) + except MatlabInterperterError: + raise + except: + raise RuntimeError('\n'.join([ + "There was an error running the code:", + code, + "-----------------------", + "Are you sure Matlab is started?", + ])) + + text_output = result_dict['content']['stdout'] # Figures get saved by matlab in reverse order... imgfiles = result_dict['content']['figures'][::-1] data_dir = result_dict['content']['datadir'] display_data = [] - if text_output: + if text_output and not args.silent: display_data.append(('MatlabMagic.matlab', - {'text/plain':text_output})) + {'text/plain': text_output})) for imgf in imgfiles: 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() display_data.append(('MatlabMagic.matlab', {'image/png': image})) - for tag, disp_d in display_data: - publish_display_data(tag, disp_d) + for disp_d in display_data: + publish_display_data(source=disp_d[0], data=disp_d[1]) # Delete the temporary data files created by matlab: if len(data_dir): 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 @@ -259,4 +214,4 @@ def unload_ipython_extension(ip): if _loaded: magic = ip.magics_manager.registry.pop('MatlabMagics') magic.Matlab.stop() - + _loaded = False diff --git a/pymatbridge/messenger/__init__.py b/pymatbridge/messenger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pymatbridge/messenger/make.py b/pymatbridge/messenger/make.py new file mode 100755 index 0000000..20045bc --- /dev/null +++ b/pymatbridge/messenger/make.py @@ -0,0 +1,291 @@ +#!/usr/bin/python + +""" +Make : building messenger mex file. + +Some functions have been taken from the pexpect module (https://pexpect.readthedocs.org/en/latest/) + +The license for pexpect is below: + + This license is approved by the OSI and FSF as GPL-compatible. + http://opensource.org/licenses/isc-license.txt + + Copyright (c) 2012, Noah Spurrier + PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY + PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE + COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +""" + +from __future__ import print_function +import os +import platform +import sys +import shlex +import shutil +import subprocess +import stat + +try: + import pty +except ImportError: + pty = None + +def is_executable_file(path): + """Checks that path is an executable regular file (or a symlink to a file). + + This is roughly ``os.path isfile(path) and os.access(path, os.X_OK)``, but + on some platforms :func:`os.access` gives us the wrong answer, so this + checks permission bits directly. + + Note + ---- + This function is taken from the pexpect module, see module doc-string for + license. + """ + # follow symlinks, + fpath = os.path.realpath(path) + + # return False for non-files (directories, fifo, etc.) + if not os.path.isfile(fpath): + return False + + # On Solaris, etc., "If the process has appropriate privileges, an + # implementation may indicate success for X_OK even if none of the + # execute file permission bits are set." + # + # For this reason, it is necessary to explicitly check st_mode + + # get file mode using os.stat, and check if `other', + # that is anybody, may read and execute. + mode = os.stat(fpath).st_mode + if mode & stat.S_IROTH and mode & stat.S_IXOTH: + return True + + # get current user's group ids, and check if `group', + # when matching ours, may read and execute. + user_gids = os.getgroups() + [os.getgid()] + if (os.stat(fpath).st_gid in user_gids and + mode & stat.S_IRGRP and mode & stat.S_IXGRP): + return True + + # finally, if file owner matches our effective userid, + # check if `user', may read and execute. + user_gids = os.getgroups() + [os.getgid()] + if (os.stat(fpath).st_uid == os.geteuid() and + mode & stat.S_IRUSR and mode & stat.S_IXUSR): + return True + + return False + + +def which(filename): + '''This takes a given filename; tries to find it in the environment path; + then checks if it is executable. This returns the full path to the filename + if found and executable. Otherwise this returns None. + + Note + ---- + This function is taken from the pexpect module, see module doc-string for + license. + ''' + + # Special case where filename contains an explicit path. + if os.path.dirname(filename) != '' and is_executable_file(filename): + return filename + if 'PATH' not in os.environ or os.environ['PATH'] == '': + p = os.defpath + else: + p = os.environ['PATH'] + pathlist = p.split(os.pathsep) + for path in pathlist: + ff = os.path.join(path, filename) + if pty: + if is_executable_file(ff): + return ff + else: + pathext = os.environ.get('Pathext', '.exe;.com;.bat;.cmd') + pathext = pathext.split(os.pathsep) + [''] + for ext in pathext: + if os.access(ff + ext, os.X_OK): + return ff + ext + return None + + +use_shell = True if sys.platform.startswith("win32") else False + + +def make_str(byte_or_str): + return byte_or_str if isinstance(byte_or_str, str) \ + else str(byte_or_str.decode("UTF-8")) + + +def esc(path): + if ' ' in path: + return '"' + path + '"' + else: + return path + + +def get_messenger_dir(): + # Check the system platform first + splatform = sys.platform + + if splatform.startswith('linux'): + messenger_dir = 'mexa64' + elif splatform.startswith('darwin'): + messenger_dir = 'mexmaci64' + elif splatform.startswith('win32'): + if splatform == "win32": + # We have a win64 messenger, so we need to figure out if this is 32 + # or 64 bit Windows: + if not platform.machine().endswith('64'): + raise ValueError("pymatbridge does not work on win32") + + # We further need to differentiate 32 from 64 bit: + maxint = sys.maxsize + if maxint == 9223372036854775807: + messenger_dir = 'mexw64' + elif maxint == 2147483647: + messenger_dir = 'mexw32' + return messenger_dir + + +def get_config(): + messenger_dir = get_messenger_dir() + with open(os.path.join(messenger_dir, 'local.cfg')) as fid: + lines = fid.readlines() + + cfg = {} + for line in lines: + if '=' not in line: + continue + name, path = line.split('=') + cfg[name.lower()] = path.strip() or '.' + return cfg + + +def do_build(make_cmd, messenger_exe): + print('Building %s...' % messenger_exe) + print(make_cmd) + messenger_dir = get_messenger_dir() + subprocess.check_output(shlex.split(make_cmd), shell=use_shell) + + messenger_loc = os.path.join(messenger_dir, messenger_exe) + + shutil.move(messenger_exe, messenger_loc) + + if os.path.exists('messenger.o'): + os.remove('messenger.o') + + +def build_octave(): + paths = "-L%(octave_lib)s -I%(octave_inc)s -L%(zmq_lib)s -I%(zmq_inc)s" + paths = paths % get_config() + make_cmd = "mkoctfile --mex %s -lzmq ./src/messenger.c" % paths + do_build(make_cmd, 'messenger.mex') + + +def which_matlab(): + try: + matlab_path = which('matlab').strip() + matlab_path = make_str(matlab_path) + return os.path.dirname(os.path.realpath(matlab_path)) + except (OSError, subprocess.CalledProcessError): + def ensure_path(path, extension=''): + return os.path.isdir(path) and \ + os.path.isfile(os.path.join(path, "matlab" + extension)) + + # need to guess the location of MATLAB + if sys.platform.startswith("darwin"): + MATLABs = [os.path.join("/Applications", i, "bin") + for i in os.listdir("/Applications") + if i.startswith("MATLAB_R")] + # only want ones with MATLAB executables + # sort so we can get the latest + MATLABs = list(sorted(filter(ensure_path, MATLABs))) + return MATLABs[-1] if len(MATLABs) > 0 else None + elif sys.platform.startswith("win32"): + MATLAB_loc = "C:\\Program Files\\MATLAB" + print(MATLAB_loc) + if not os.path.isdir(MATLAB_loc): + return None + MATLABs = [os.path.join(MATLAB_loc, i, "bin") + for i in os.listdir(MATLAB_loc)] + print(MATLABs) + print(i) + # only want ones with MATLAB executables + # sort so we can get the latest + MATLABs = list(sorted(filter(lambda x: ensure_path(x, ".exe"), + MATLABs))) + print(MATLABs) + return MATLABs[-1] if len(MATLABs) > 0 else None + elif sys.platform.startswith("linux"): + MATLAB_loc = "/usr/local/MATLAB/" + if not os.path.isdir(MATLAB_loc): + return None + MATLABs = [os.path.join(MATLAB_loc, i, "bin") + for i in os.listdir(MATLAB_loc) + if i.startswith("R")] + # only want ones with MATLAB executables + # sort so we can get the latest + MATLABs = list(sorted(filter(ensure_path, MATLABs))) + return MATLABs[-1] if len(MATLABs) > 0 else None + + +def build_matlab(static=False): + """build the messenger mex for MATLAB + + static : bool + Determines if the zmq library has been statically linked. + If so, it will append the command line option -DZMQ_STATIC + when compiling the mex so it matches libzmq. + """ + cfg = get_config() + # To deal with spaces, remove quotes now, and add + # to the full commands themselves. + if 'matlab_bin' in cfg and cfg['matlab_bin'] != '.': + matlab_bin = cfg['matlab_bin'].strip('"') + else: # attempt to autodetect MATLAB filepath + matlab_bin = which_matlab() + if matlab_bin is None: + raise ValueError("specify 'matlab_bin' in cfg file") + # Get the extension + extcmd = esc(os.path.join(matlab_bin, "mexext")) + extension = subprocess.check_output(extcmd, shell=use_shell) + extension = extension.decode('utf-8').rstrip('\r\n') + + # Build the mex file + mex = esc(os.path.join(matlab_bin, "mex")) + paths = "-L%(zmq_lib)s -I%(zmq_inc)s" % cfg + make_cmd = '%s -O %s -lzmq ./src/messenger.c' % (mex, paths) + if static: + make_cmd += ' -DZMQ_STATIC' + do_build(make_cmd, 'messenger.%s' % extension) + + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser() + parser.add_argument( + "target", + choices=["matlab", "octave"], + type=str.lower, + help="target to be built") + parser.add_argument("--static", action="store_true", + help="staticly link libzmq") + args = parser.parse_args() + if args.target == "matlab": + build_matlab(static=args.static) + elif args.target == "octave": + if args.static: + raise ValueError("static building not yet supported for octave") + build_octave() + else: + raise ValueError() diff --git a/pymatbridge/messenger/mexa64/local.cfg b/pymatbridge/messenger/mexa64/local.cfg new file mode 100644 index 0000000..a9a742c --- /dev/null +++ b/pymatbridge/messenger/mexa64/local.cfg @@ -0,0 +1,6 @@ +MATLAB_BIN= +OCTAVE_INC=/usr/include +OCTAVE_LIB=/usr/lib/x86_64-linux-gnu/ +ZMQ_INC= +ZMQ_LIB= + diff --git a/pymatbridge/messenger/mexa64/messenger.mex b/pymatbridge/messenger/mexa64/messenger.mex new file mode 100755 index 0000000..85afaa9 Binary files /dev/null and b/pymatbridge/messenger/mexa64/messenger.mex differ diff --git a/pymatbridge/messenger/mexa64/messenger.mex.zmq4 b/pymatbridge/messenger/mexa64/messenger.mex.zmq4 new file mode 100755 index 0000000..65e467c Binary files /dev/null and b/pymatbridge/messenger/mexa64/messenger.mex.zmq4 differ diff --git a/pymatbridge/messenger/mexa64/messenger.mexa64 b/pymatbridge/messenger/mexa64/messenger.mexa64 new file mode 100755 index 0000000..f585eab Binary files /dev/null and b/pymatbridge/messenger/mexa64/messenger.mexa64 differ diff --git a/pymatbridge/messenger/mexmaci64/local.cfg b/pymatbridge/messenger/mexmaci64/local.cfg new file mode 100644 index 0000000..138795a --- /dev/null +++ b/pymatbridge/messenger/mexmaci64/local.cfg @@ -0,0 +1,5 @@ +MATLAB_BIN= +ZMQ_INC= +ZMQ_LIB= +OCTAVE_INC= +OCTAVE_LIB= diff --git a/pymatbridge/messenger/mexmaci64/messenger.mex b/pymatbridge/messenger/mexmaci64/messenger.mex new file mode 100755 index 0000000..27dbcd6 Binary files /dev/null and b/pymatbridge/messenger/mexmaci64/messenger.mex differ diff --git a/pymatbridge/messenger/mexmaci64/messenger.mex.zmq3 b/pymatbridge/messenger/mexmaci64/messenger.mex.zmq3 new file mode 100755 index 0000000..c394ada Binary files /dev/null and b/pymatbridge/messenger/mexmaci64/messenger.mex.zmq3 differ diff --git a/pymatbridge/messenger/mexmaci64/messenger.mexmaci64 b/pymatbridge/messenger/mexmaci64/messenger.mexmaci64 new file mode 100755 index 0000000..a48edc0 Binary files /dev/null and b/pymatbridge/messenger/mexmaci64/messenger.mexmaci64 differ diff --git a/pymatbridge/messenger/mexmaci64/messenger.mexmaci64.zmq3 b/pymatbridge/messenger/mexmaci64/messenger.mexmaci64.zmq3 new file mode 100755 index 0000000..dc77206 Binary files /dev/null and b/pymatbridge/messenger/mexmaci64/messenger.mexmaci64.zmq3 differ diff --git a/pymatbridge/messenger/mexw64/README.md b/pymatbridge/messenger/mexw64/README.md new file mode 100644 index 0000000..47100e8 --- /dev/null +++ b/pymatbridge/messenger/mexw64/README.md @@ -0,0 +1,41 @@ +Dynamically linked building instructions +---------------------------------------- + +1) Install zeromq from the website: http://zeromq.org/distro:microsoft-windows + +2) Rename one of the lib/libzmq-*.lib files to libzmq.lib in the ZeroMQ + installation directory + +3) Add the ZeroMQ bin directory to your path. + +4) Edit the messenger/mexw64/local.cfg file in messenger to point to the + zeromq install directory (you will need to update both ZMQ_INC and ZMQ_LIB). + Also ensure the MATLAB directory is correct. + +5) Run ```make.py matlab``` in the messenger directory. This should build + messenger.mexw64 + +Statically linked building instructions +--------------------------------------- + +A statically linked library has the advantage of not requiring libzmq.dll to +be found in the path. For this reason, including it in the installer results +in a simpler and more robust installation process. While building a statically +linked mex is simple in practice, but because zeromq (as of 3/10/15) does not +provide a .lib for static linking with the windows installer, you will need to +compile this yourself. These directions are from zeromq 4.0.5. + +1) Download and unzip the zeromq zip file (listed as Windows sources) from + http://zeromq.org/intro:get-the-software + +2) In the builds/msvc directory open the msvc.sln file in Visual Studio. + +3) Create a new Platform for x64. In the Librarian section of properties, set + the target section to /Machine:X64 + +4) Build libzmq with the "StaticRelease" for x64. + +5) Edit the messenger/mexw64/local.cfg file to point to where you built ZeroMQ + and your MATLAB bin directory. + +6) Build messenger.mexw64 with ```make matlab --static``` diff --git a/pymatbridge/messenger/mexw64/local.cfg b/pymatbridge/messenger/mexw64/local.cfg new file mode 100644 index 0000000..ab61682 --- /dev/null +++ b/pymatbridge/messenger/mexw64/local.cfg @@ -0,0 +1,5 @@ +MATLAB_BIN="C:\Program Files\MATLAB\2013a\bin" +OCTAVE_INC="C:\Octave\Octave-3.8.2\include\octave-3.8.2\octave" +OCTAVE_LIB="C:\Octave\Octave-3.8.2\lib\octave\3.8.2" +ZMQ_INC="C:\zeromq-4.0.5\include" +ZMQ_LIB="C:\zeromq-4.0.5\lib" diff --git a/pymatbridge/messenger/mexw64/messenger.mex b/pymatbridge/messenger/mexw64/messenger.mex new file mode 100644 index 0000000..59fb4e0 Binary files /dev/null and b/pymatbridge/messenger/mexw64/messenger.mex differ diff --git a/pymatbridge/messenger/mexw64/messenger.mexw64 b/pymatbridge/messenger/mexw64/messenger.mexw64 new file mode 100644 index 0000000..3f0abd5 Binary files /dev/null and b/pymatbridge/messenger/mexw64/messenger.mexw64 differ diff --git a/messenger/src/messenger.c b/pymatbridge/messenger/src/messenger.c similarity index 70% rename from messenger/src/messenger.c rename to pymatbridge/messenger/src/messenger.c index b9cca29..102999e 100644 --- a/messenger/src/messenger.c +++ b/pymatbridge/messenger/src/messenger.c @@ -1,20 +1,23 @@ #include #include +#include #include "mex.h" #include "zmq.h" /* Set a 200MB receiver buffer size */ #define BUFLEN 200000000 -void *ctx, *socket; +/* The variable cannot be named socket on windows */ +void *ctx, *socket_ptr; static int initialized = 0; /* Initialize a ZMQ server */ int initialize(char *socket_addr) { - + int rc; + mexLock(); ctx = zmq_ctx_new(); - socket = zmq_socket(ctx, ZMQ_REP); - int rc = zmq_bind(socket, socket_addr); + socket_ptr = zmq_socket(ctx, ZMQ_REP); + rc = zmq_bind(socket_ptr, socket_addr); if (!rc) { initialized = 1; @@ -24,35 +27,24 @@ int initialize(char *socket_addr) { } } -/* Listen over an existing socket - * Now the receiver buffer is pre-allocated - * In the future we can possibly use multi-part messaging - */ -int listen(char *buffer, int buflen) { +/* Check if the ZMQ server is intialized and print an error if not */ +int checkInitialized(void) { if (!initialized) { mexErrMsgTxt("Error: ZMQ session not initialized"); + return 0; } - - return zmq_recv(socket, buffer, buflen, 0); -} - -/* Sending out a message */ -int respond(char *msg_out, int len) { - if (!initialized) { - mexErrMsgTxt("Error: ZMQ session not initialized"); + else { + return 1; } - - int bytesent = zmq_send(socket, msg_out, len, 0); - - return bytesent; } + /* Cleaning up after session finished */ void cleanup (void) { /* Send a confirmation message to the client */ - zmq_send(socket, "exit", 4, 0); + zmq_send(socket_ptr, "exit", 4, 0); - zmq_close(socket); + zmq_close(socket_ptr); mexPrintf("Socket closed\n"); zmq_term(ctx); mexPrintf("Context terminated\n"); @@ -62,14 +54,13 @@ void cleanup (void) { /* Gateway function with Matlab */ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { - + char *cmd; /* If no input argument, print out the usage */ if (nrhs == 0) { mexErrMsgTxt("Usage: messenger('init|listen|respond', extra1, extra2, ...)"); } /* Get the input command */ - char *cmd; if(!(cmd = mxArrayToString(prhs[0]))) { mexErrMsgTxt("Cannot read the command"); } @@ -92,10 +83,10 @@ void mexFunction(int nlhs, mxArray *plhs[], if (!initialized) { if (!initialize(socket_addr)) { - p[0] = true; + p[0] = 1; mexPrintf("Socket created at: %s\n", socket_addr); } else { - p[0] = false; + p[0] = 0; mexErrMsgTxt("Socket creation failed."); } } else { @@ -106,9 +97,18 @@ void mexFunction(int nlhs, mxArray *plhs[], /* Listen over an existing socket */ } else if (strcmp(cmd, "listen") == 0) { + int byte_recvd; char *recv_buffer = mxCalloc(BUFLEN, sizeof(char)); + zmq_pollitem_t polls[] = {{socket_ptr, 0, ZMQ_POLLIN, 0}}; + + if (!checkInitialized()) return; + + /* allow MATLAB to draw its graphics every 20ms */ + while (zmq_poll(polls, 1, 20000) == 0) { + mexEvalString("drawnow"); + } - int byte_recvd = listen(recv_buffer, BUFLEN); + byte_recvd = zmq_recv(socket_ptr, recv_buffer, BUFLEN, 0); /* Check if the received data is complete and correct */ if ((byte_recvd > -1) && (byte_recvd <= BUFLEN)) { @@ -116,13 +116,16 @@ void mexFunction(int nlhs, mxArray *plhs[], } else if (byte_recvd > BUFLEN){ mexErrMsgTxt("Receiver buffer overflow. Message truncated"); } else { - mexErrMsgTxt("Failed to receive a message due to ZMQ error"); + sprintf(recv_buffer, "Failed to receive a message due to ZMQ error %s", strerror(errno)); + mexErrMsgTxt(recv_buffer); } return; /* Send a message out */ } else if (strcmp(cmd, "respond") == 0) { + size_t msglen; + char *msg_out; mxLogical *p; /* Check if the input format is valid */ @@ -130,16 +133,18 @@ void mexFunction(int nlhs, mxArray *plhs[], mexErrMsgTxt("Please provide the message to send"); } - size_t msglen = mxGetNumberOfElements(prhs[1]); - char *msg_out = mxArrayToString(prhs[1]); + if (!checkInitialized()) return; + + msglen = mxGetNumberOfElements(prhs[1]); + msg_out = mxArrayToString(prhs[1]); plhs[0] = mxCreateLogicalMatrix(1, 1); p = mxGetLogicals(plhs[0]); - if (msglen == respond(msg_out, msglen)) { - p[0] = true; + if (msglen == zmq_send(socket_ptr, msg_out, msglen, 0)) { + p[0] = 1; } else { - p[0] = false; + p[0] = 0; mexErrMsgTxt("Failed to send message due to ZMQ error"); } diff --git a/pymatbridge/publish.py b/pymatbridge/publish.py new file mode 100644 index 0000000..213d3cb --- /dev/null +++ b/pymatbridge/publish.py @@ -0,0 +1,135 @@ +try: + import nbformat.v4 as nbformat + from nbformat import write as nbwrite +except ImportError: + import IPython.nbformat.v4 as nbformat + from IPython.nbformat import write as nbwrite + +import numpy as np + + +def format_line(line): + """ + Format a line of Matlab into either a markdown line or a code line. + + Parameters + ---------- + line : str + The line of code to be formatted. Formatting occurs according to the + following rules: + + - If the line starts with (at least) two %% signs, a new cell will be + started. + + - If the line doesn't start with a '%' sign, it is assumed to be legit + matlab code. We will continue to add to the same cell until reaching + the next comment line + """ + if line.startswith('%%'): + md = True + new_cell = True + source = line.split('%%')[1] + '\n' # line-breaks in md require a line + # gap! + + elif line.startswith('%'): + md = True + new_cell = False + source = line.split('%')[1] + '\n' + + else: + md = False + new_cell = False + source = line + + + return new_cell, md, source + + +def mfile_to_lines(mfile): + """ + Read the lines from an mfile + + Parameters + ---------- + mfile : string + Full path to an m file + """ + # We should only be able to read this file: + with open(mfile) as fid: + return fid.readlines() + + +def lines_to_notebook(lines, name=None): + """ + Convert the lines of an m file into an IPython notebook + + Parameters + ---------- + lines : list + A list of strings. Each element is a line in the m file + + Returns + ------- + notebook : an IPython NotebookNode class instance, containing the + information required to create a file + + + """ + source = [] + md = np.empty(len(lines), dtype=object) + new_cell = np.empty(len(lines), dtype=object) + for idx, l in enumerate(lines): + new_cell[idx], md[idx], this_source = format_line(l) + # Transitions between markdown and code and vice-versa merit a new + # cell, even if no newline, or "%%" is found. Make sure not to do this + # check for the very first line! + if idx>1 and not new_cell[idx]: + if md[idx] != md[idx-1]: + new_cell[idx] = True + + source.append(this_source) + # This defines the breaking points between cells: + new_cell_idx = np.hstack([np.where(new_cell)[0], -1]) + + # Listify the sources: + cell_source = [source[new_cell_idx[i]:new_cell_idx[i+1]] + for i in range(len(new_cell_idx)-1)] + cell_md = [md[new_cell_idx[i]] for i in range(len(new_cell_idx)-1)] + cells = [] + + # Append the notebook with loading matlab magic extension + notebook_head = "import pymatbridge as pymat\n" + "ip = get_ipython()\n" \ + + "pymat.load_ipython_extension(ip)" + cells.append(nbformat.new_code_cell(notebook_head))#, language='python')) + + for cell_idx, cell_s in enumerate(cell_source): + if cell_md[cell_idx]: + cells.append(nbformat.new_markdown_cell(cell_s)) + else: + cell_s.insert(0, '%%matlab\n') + cells.append(nbformat.new_code_cell(cell_s))#, language='matlab')) + + #ws = nbformat.new_worksheet(cells=cells) + notebook = nbformat.new_notebook(cells=cells) + return notebook + + +def convert_mfile(mfile, outfile=None): + """ + Convert a Matlab m-file into a Matlab notebook in ipynb format + + Parameters + ---------- + mfile : string + Full path to a matlab m file to convert + + outfile : string (optional) + Full path to the output ipynb file + + """ + lines = mfile_to_lines(mfile) + nb = lines_to_notebook(lines) + if outfile is None: + outfile = mfile.split('.m')[0] + '.ipynb' + with open(outfile, 'w') as fid: + nbwrite(nb, fid) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 5a135db..e107532 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -2,62 +2,127 @@ pymatbridge =========== -This is a module for communicating and running +This is a module for communicating and running Matlab from within python + +Example +------- + +>>> import pymatbridge +>>> m = pymatbridge.Matlab() +>>> m.start() +Starting MATLAB on ZMQ socket ipc:///tmp/pymatbridge +Send 'exit' command to kill the server +.MATLAB started and connected! +True +>>> m.run_code('a=1;') +{'content': {'stdout': '', 'datadir': '/private/tmp/MatlabData/', 'code': 'a=1;', 'figures': []}, 'success': True} +>>> m.get_variable('a') +1 -Part of Python-MATLAB-bridge, Max Jaderberg 2012 - -This is a modified version using ZMQ, Haoxing Zhang Jan.2014 """ -import numpy as np -import os, time +import atexit +import os +import time +import base64 import zmq import subprocess -import platform import sys - import json +import types +import weakref +import random +from uuid import uuid4 + +from numpy import ndarray, generic, float64, frombuffer, asfortranarray + +from pymatbridge.messenger.make import get_messenger_dir + +try: + from scipy.sparse import spmatrix +except ImportError: + class spmatrix: + pass + + +def encode_ndarray(obj): + """Write a numpy array and its shape to base64 buffers""" + shape = obj.shape + if len(shape) == 1: + shape = (1, obj.shape[0]) + if obj.flags.c_contiguous: + obj = obj.T + elif not obj.flags.f_contiguous: + obj = asfortranarray(obj.T) + else: + obj = obj.T + try: + data = obj.astype(float64).tobytes() + except AttributeError: + data = obj.astype(float64).tostring() + + data = base64.b64encode(data).decode('utf-8') + return data, shape + + +# JSON encoder extension to handle complex numbers and numpy arrays +class PymatEncoder(json.JSONEncoder): -# JSON encoder extension to handle complex numbers -class ComplexEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, complex): - return {'real':obj.real, 'imag':obj.imag} + if isinstance(obj, ndarray) and obj.dtype.kind in 'uif': + data, shape = encode_ndarray(obj) + return {'ndarray': True, 'shape': shape, 'data': data} + elif isinstance(obj, ndarray) and obj.dtype.kind == 'c': + real, shape = encode_ndarray(obj.real.copy()) + imag, _ = encode_ndarray(obj.imag.copy()) + return {'ndarray': True, 'shape': shape, + 'real': real, 'imag': imag} + elif isinstance(obj, ndarray): + return obj.tolist() + elif isinstance(obj, complex): + return {'real': obj.real, 'imag': obj.imag} + elif isinstance(obj, generic): + return obj.item() # Handle the default case return json.JSONEncoder.default(self, obj) -# JSON decoder for complex numbers -def as_complex(dct): - if 'real' in dct and 'imag' in dct: + +def decode_arr(data): + """Extract a numpy array from a base64 buffer""" + data = data.encode('utf-8') + 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 = dct['shape'] + if type(dct['shape']) is not list: + 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']).astype(int) + data = real + 1j * imag + return data.reshape(shape, order='F') + elif 'real' in dct and 'imag' in dct: return complex(dct['real'], dct['imag']) return dct - MATLAB_FOLDER = '%s/matlab' % os.path.realpath(os.path.dirname(__file__)) +MESSENGER_FOLDER = '%s/messenger/%s' % (os.path.realpath(os.path.dirname(__file__)), get_messenger_dir()) -# Start a Matlab server and bind it to a ZMQ socket(TCP/IPC) -def _run_matlab_server(matlab_bin, matlab_socket_addr, matlab_log, matlab_id, matlab_startup_options): - command = matlab_bin - command += ' %s ' % matlab_startup_options - command += ' -r "' - command += "addpath(genpath(" - command += "'%s'" % MATLAB_FOLDER - command += ')), matlabserver(\'%s\'),exit"' % matlab_socket_addr - - if matlab_log: - command += ' -logfile ./pymatbridge/logs/matlablog_%s.txt > ./pymatbridge/logs/bashlog_%s.txt' % (matlab_id, matlab_id) - subprocess.Popen(command, shell = True) - - return True - - -class Matlab(object): +class _Session(object): """ - A class for communicating with a matlab session + A class for communicating with a MATLAB session. It provides the behavior + common across different MATLAB implementations. You shouldn't instantiate + this directly; rather, use the Matlab or Octave subclasses. """ - def __init__(self, matlab='matlab', socket_addr='ipc:///tmp/pymatbridge', + def __init__(self, executable, socket_addr=None, id='python-matlab-bridge', log=False, maxtime=60, platform=None, startup_options=None): """ @@ -66,86 +131,119 @@ def __init__(self, matlab='matlab', socket_addr='ipc:///tmp/pymatbridge', Parameters ---------- - matlab : str - A string that woul start matlab at the terminal. Per default, this - is set to 'matlab', so that you can alias in your bash setup + executable : str + A string that would start the session at the terminal. socket_addr : str A string that represents a valid ZMQ socket address, such as - "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. Default is + to choose a random IPC file name, or a random socket (for TCP). id : str - An identifier for this instance of the pymatbridge + An identifier for this instance of the pymatbridge. log : bool Whether to save a log file in some known location. maxtime : float - The maximal time to wait for a response from matlab (optional, + The maximal time to wait for a response from the session (optional, Default is 10 sec) platform : string The OS of the machine on which this is running. Per default this will be taken from sys.platform. + startup_options : string + Command line options to include in the executable's invocation. + Optional; sensible defaults are used if this is not provided. """ - # Setup internal state variables self.started = False - self.running = False - self.matlab = matlab + self.executable = executable self.socket_addr = socket_addr - self.id = id self.log = log self.maxtime = maxtime + self.platform = platform if platform is not None else sys.platform + self.startup_options = startup_options - if platform is None: - self.platform = sys.platform - else: - self.platform = platform + if socket_addr is None: + self.socket_addr = "tcp://127.0.0.1" if self.platform == "win32" else "ipc:///tmp/pymatbridge-%s"%str(uuid4()) - if startup_options: - self.startup_options = startup_options - elif self.platform == 'Windows': - self.startup_options = ' -automation -noFigureWindows' - else: - self.startup_options = ' -nodesktop -nodisplay' + if self.log: + startup_options += ' > ./pymatbridge/logs/bashlog_%s.txt' % self.id self.context = None self.socket = None + atexit.register(self.stop) + + def _program_name(self): # pragma: no cover + raise NotImplemented + + def _preamble_code(self): + # suppress warnings while loading the path, in the case of + # overshadowing a built-in function on a newer version of + # Matlab (e.g. isrow) + return ["old_warning_state = warning('off','all');", + "addpath(genpath('%s'));" % MATLAB_FOLDER, + "addpath('%s');" % MESSENGER_FOLDER, + "warning(old_warning_state);", + "clear('old_warning_state');", + "cd('%s');" % os.getcwd()] + + def _execute_flag(self): # pragma: no cover + raise NotImplemented + + def _run_server(self): + code = self._preamble_code() + code.extend([ + "matlabserver('%s')" % self.socket_addr + ]) + command = '%s %s %s "%s"' % (self.executable, self.startup_options, + self._execute_flag(), ','.join(code)) + subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, + stdout=subprocess.PIPE) # Start server/client session and make the connection def start(self): + # Setup socket + self.context = zmq.Context() + self.socket = self.context.socket(zmq.REQ) + if self.platform == "win32": + rndport = random.randrange(49152, 65536) + self.socket_addr = self.socket_addr + ":%s"%rndport + # Start the MATLAB server in a new process - print "Starting MATLAB on ZMQ socket %s" % (self.socket_addr) - print "Send 'exit' command to kill the server" - _run_matlab_server(self.matlab, self.socket_addr, self.log, self.id, self.startup_options) + print("Starting %s on ZMQ socket %s" % (self._program_name(), self.socket_addr)) + print("Send 'exit' command to kill the server") + self._run_server() # Start the client - self.context = zmq.Context() - self.socket = self.context.socket(zmq.REQ) self.socket.connect(self.socket_addr) self.started = True # Test if connection is established - if (self.is_connected()): - print "MATLAB started and connected!" - return True + if self.is_connected(): + print("%s started and connected!" % self._program_name()) + self.set_plot_settings() + return self else: - print "MATLAB failed to start" - return False + raise ValueError("%s failed to start" % self._program_name()) + def _response(self, **kwargs): + req = json.dumps(kwargs, cls=PymatEncoder) + self.socket.send_string(req) + resp = self.socket.recv_string() + return resp # Stop the Matlab server def stop(self): - req = json.dumps(dict(cmd="exit"), cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() + if not self.started: + return True # Matlab should respond with "exit" if successful - if resp == "exit": - print "MATLAB closed" + if self._response(cmd='exit') == "exit": + print("%s closed" % self._program_name()) self.started = False return True @@ -156,73 +254,328 @@ def is_connected(self): time.sleep(2) return False - req = json.dumps(dict(cmd="connect"), cls=ComplexEncoder) - self.socket.send(req) + req = json.dumps(dict(cmd="connect"), cls=PymatEncoder) + self.socket.send_string(req) start_time = time.time() - while(True): + while True: try: resp = self.socket.recv_string(flags=zmq.NOBLOCK) - if resp == "connected": - return True - else: - return False + return resp == "connected" except zmq.ZMQError: - np.disp(".", linefeed=False) + sys.stdout.write('.') time.sleep(1) - if (time.time() - start_time > self.maxtime) : - print "Matlab session timed out after %d seconds" % (self.maxtime) + if time.time() - start_time > self.maxtime: + print("%s session timed out after %d seconds" % (self._program_name(), self.maxtime)) return False - def is_function_processor_working(self): - result = self.run_func('%s/test_functions/test_sum.m' % MATLAB_FOLDER, {'echo': 'Matlab: Function processor is working!'}) - if result['success'] == 'true': - return True + result = self.run_func('%s/usrprog/test_sum.m' % MATLAB_FOLDER, + {'echo': '%s: Function processor is working!' % self._program_name()}) + return result['success'] + + def _json_response(self, **kwargs): + return json.loads(self._response(**kwargs), object_hook=decode_pymat) + + 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, optional + Function args to send to the function. + 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' + """ + if not self.started: + raise ValueError('Session not started, use start()') + + nargout = kwargs.pop('nargout', 1) + func_args += tuple(item for pair in zip(kwargs.keys(), kwargs.values()) + for item in pair) + dname = os.path.dirname(func_path) + fname = os.path.basename(func_path) + func_name, ext = os.path.splitext(fname) + if ext and not ext == '.m': + raise TypeError('Need to give path to .m file') + return self._json_response(cmd='eval', + func_name=func_name, + func_args=func_args or '', + dname=dname, + nargout=nargout) + + def run_code(self, code): + """Run some code in Matlab command line provide by a string + + Parameters + ---------- + code : str + Code to send for evaluation. + """ + return self.run_func('evalin', 'base', code, nargout=0) + + def get_variable(self, varname, default=None): + resp = self.run_func('evalin', 'base', varname) + return resp['result'] if resp['success'] else default + + def set_variable(self, varname, value): + if isinstance(value, spmatrix): + return self._set_sparse_variable(varname, value) + return self.run_func('assignin', 'base', varname, value, nargout=0) + + def set_plot_settings(self, width=512, height=384, inline=True): + if inline: + code = ["set(0, 'defaultfigurevisible', 'off')"] else: - return False + code = ["set(0, 'defaultfigurevisible', 'on')"] + size = "set(0, 'defaultfigurepaperposition', [0 0 %s %s])" + code += ["set(0, 'defaultfigurepaperunits', 'inches')", + "set(0, 'defaultfigureunits', 'inches')", + size % (int(width) / 150., int(height) / 150.)] + self.run_code(';'.join(code)) + + def _set_sparse_variable(self, varname, value): + value = value.todok() + prefix = 'pymatbridge_temp_sparse_%s_' % uuid4().hex + self.set_variable(prefix + 'keys', list(value.keys())) + # correct for 1-indexing in MATLAB + self.run_code('{0}keys = {0}keys + 1;'.format(prefix)) + self.set_variable(prefix + 'values', list(value.values())) + cmd = "{1} = sparse({0}keys(:, 1), {0}keys(:, 2), {0}values');" + result = self.run_code(cmd.format(prefix, varname)) + 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 - # Run a function in Matlab and return the result - def run_func(self, func_path, func_args=None, maxtime=None): - if self.running: - time.sleep(0.05) + """ + # 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) - req = dict(cmd="run_function") - req['func_path'] = func_path - req['func_args'] = func_args - req = json.dumps(req, cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() - resp = json.loads(resp, object_hook=as_complex) +class Matlab(_Session): + def __init__(self, executable='matlab', socket_addr=None, + id='python-matlab-bridge', log=False, maxtime=60, + platform=None, startup_options=None): + """ + Initialize this thing. - return resp + Parameters + ---------- + + executable : str + A string that would start Matlab at the terminal. Per default, this + is set to 'matlab', so that you can alias in your bash setup - # Run some code in Matlab command line provide by a string - def run_code(self, code, maxtime=None): - if self.running: - time.sleep(0.05) + socket_addr : str + A string that represents a valid ZMQ socket address, such as + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. - req = dict(cmd="run_code") - req['code'] = code - req = json.dumps(req, cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() - resp = json.loads(resp, object_hook=as_complex) + id : str + An identifier for this instance of the pymatbridge. - return resp + log : bool + Whether to save a log file in some known location. - def get_variable(self, varname, maxtime=None): - if self.running: - time.sleep(0.05) + maxtime : float + The maximal time to wait for a response from matlab (optional, + Default is 10 sec) - req = dict(cmd="get_var") - req['varname'] = varname - req = json.dumps(req, cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() - resp = json.loads(resp, object_hook=as_complex) + platform : string + The OS of the machine on which this is running. Per default this + will be taken from sys.platform. + + startup_options : string + Command line options to pass to MATLAB. Optional; sensible defaults + are used if this is not provided. + """ + if platform is None: + platform = sys.platform + if startup_options is None: + if platform == 'win32': + startup_options = ' -automation -nosplash' + else: + startup_options = ' -nodesktop -nosplash' + if log: + startup_options += ' -logfile ./pymatbridge/logs/matlablog_%s.txt' % id + super(Matlab, self).__init__(executable, socket_addr, id, log, maxtime, + platform, startup_options) + + def _program_name(self): + return 'MATLAB' + + def _execute_flag(self): + return '-r' + + +class Octave(_Session): + def __init__(self, executable='octave', socket_addr=None, + id='python-matlab-bridge', log=False, maxtime=60, + platform=None, startup_options=None): + """ + Initialize this thing. + + Parameters + ---------- + + executable : str + A string that would start Octave at the terminal. Per default, this + is set to 'octave', so that you can alias in your bash setup + + socket_addr : str + A string that represents a valid ZMQ socket address, such as + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. + + id : str + An identifier for this instance of the pymatbridge. - return resp['var'] + log : bool + Whether to save a log file in some known location. + + maxtime : float + The maximal time to wait for a response from octave (optional, + Default is 10 sec) + + platform : string + The OS of the machine on which this is running. Per default this + will be taken from sys.platform. + + startup_options : string + Command line options to pass to Octave. Optional; sensible defaults + are used if this is not provided. + """ + if startup_options is None: + startup_options = '--silent --no-gui' + super(Octave, self).__init__(executable, socket_addr, id, log, maxtime, + platform, startup_options) + + def _program_name(self): + return 'Octave' + + def _preamble_code(self): + code = super(Octave, self)._preamble_code() + if self.log: + code.append("diary('./pymatbridge/logs/octavelog_%s.txt')" % self.id) + code.append("graphics_toolkit('gnuplot')") + return code + + 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 diff --git a/pymatbridge/tests/test_array.py b/pymatbridge/tests/test_array.py deleted file mode 100644 index 4d271e3..0000000 --- a/pymatbridge/tests/test_array.py +++ /dev/null @@ -1,26 +0,0 @@ -import pymatbridge as pymat -import random as rd -import numpy as np -import numpy.testing as npt -import test_utils as tu - -class TestArray: - - # Start a Matlab session before any tests - @classmethod - def setup_class(cls): - cls.mlab = tu.connect_to_matlab() - - # Tear down the Matlab session after all the tests are done - @classmethod - def teardown_class(cls): - tu.stop_matlab(cls.mlab) - - - # Pass a 1000*1000 array to Matlab - def test_array_size(self): - array = np.random.random_sample((50,50)).tolist() - res = self.mlab.run_func("array_size.m",{'val':array})['result'] - npt.assert_almost_equal(res, array, decimal=8, err_msg = "test_array_size: error") - - diff --git a/pymatbridge/tests/test_functions.py b/pymatbridge/tests/test_functions.py new file mode 100644 index 0000000..e0367fc --- /dev/null +++ b/pymatbridge/tests/test_functions.py @@ -0,0 +1,59 @@ +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'] + assert len(resp['content']['figures']) + resp = self.mlab.plot([1, 2, 3], Linewidth=3) + assert resp['result'] is not None + assert len(resp['content']['figures']) diff --git a/pymatbridge/tests/test_get_variable.py b/pymatbridge/tests/test_get_variable.py index 30cc28b..be38ea0 100644 --- a/pymatbridge/tests/test_get_variable.py +++ b/pymatbridge/tests/test_get_variable.py @@ -30,13 +30,19 @@ def test_get_array(self): self.mlab.run_code("a = [1 2 3 4]") self.mlab.run_code("b = [1 2; 3 4]") - npt.assert_equal(self.mlab.get_variable('a'), [1,2,3,4]) - npt.assert_equal(self.mlab.get_variable('b'), [[1,2],[3,4]]) + npt.assert_equal(self.mlab.get_variable('a'), [[1.,2.,3.,4.]]) + npt.assert_equal(self.mlab.get_variable('b'), [[1.,2.],[3.,4.]]) # Try to get a non-existent variable - # This one will always fail now since the matlab function cannot handle this situation -# def test_nonexistent_var(self): -# self.mlab.run_code("clear") + def test_nonexistent_var(self): + self.mlab.run_code("clear") -# npt.assert_equal(self.mlab.get_variable('a'), unicode("456345.3453")) + npt.assert_equal(self.mlab.get_variable('a'), None) + + + # Try to get a non-existent variable with default + def test_nonexistent_var_default(self): + self.mlab.run_code("clear") + + npt.assert_equal(self.mlab.get_variable('a', 'some_val'), 'some_val') diff --git a/pymatbridge/tests/test_json.py b/pymatbridge/tests/test_json.py index 86973b8..4028aff 100644 --- a/pymatbridge/tests/test_json.py +++ b/pymatbridge/tests/test_json.py @@ -1,4 +1,5 @@ import pymatbridge as pymat +from pymatbridge.compat import unichr import numpy.testing as npt import test_utils as tu diff --git a/pymatbridge/tests/test_magic.py b/pymatbridge/tests/test_magic.py index c8a8eaa..4a9d38f 100644 --- a/pymatbridge/tests/test_magic.py +++ b/pymatbridge/tests/test_magic.py @@ -1,24 +1,38 @@ +import sys +import os +from uuid import uuid4 + import pymatbridge as pymat -import IPython +from pymatbridge.matlab_magic import MatlabInterperterError +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) - - # Unload the magic, shut down Matlab + if 'USE_OCTAVE' in os.environ: + matlab = 'octave' + else: + matlab = 'matlab' + + # We will test the passing of kwargs through to the Matlab or Octave + # objects, by assigning the socket address out here: + socket_addr = "tcp://127.0.0.1" if sys.platform == "win32" else "ipc:///tmp/pymatbridge-%s"%str(uuid4()) + pymat.load_ipython_extension(cls.ip, matlab=matlab, + socket_addr=socket_addr) + + # 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 @@ -27,6 +41,7 @@ def test_cell_magic_number(self): npt.assert_almost_equal(self.ip.user_ns['b'], self.ip.user_ns['a']*2, decimal=7) + def test_cell_magic_number_complex(self): # A complex number self.ip.run_cell("x = 3.34+4.56j") self.ip.run_cell_magic('matlab', '-i x -o y', 'y = x*(11.35 - 23.098j)') @@ -35,14 +50,6 @@ def test_cell_magic_number(self): self.ip.user_ns['res'], decimal=7) - # A complex matrix: - self.ip.run_cell("x = [3.34+4.56j, 3.34+4.56j];") - self.ip.run_cell_magic('matlab', '-i x -o y', 'y = x*(11.35 - 23.098j)') - self.ip.run_cell("res = x*(11.35 - 23.098j)") - npt.assert_almost_equal(self.ip.user_ns['y'], - self.ip.user_ns['res'], decimal=7) - - def test_cell_magic_array(self): # Random array multiplication self.ip.run_cell("val1 = np.random.random_sample((3,3))") @@ -53,6 +60,14 @@ def test_cell_magic_array(self): npt.assert_almost_equal(self.ip.user_ns['resmat'], self.ip.user_ns['respy'], decimal=7) + def test_cell_magic_array_complex(self): + self.ip.run_cell("val1 = np.random.random((3,3)) + np.random.random((3,3))*1j") + self.ip.run_cell("val2 = np.random.random((3,3)) + np.random.random((3,3))*1j") + self.ip.run_cell("respy = np.dot(val1, val2)") + self.ip.run_cell_magic('matlab', '-i val1,val2 -o resmat', + 'resmat = val1 * val2') + npt.assert_almost_equal(self.ip.user_ns['resmat'], + self.ip.user_ns['respy'], decimal=7) def test_line_magic(self): # Some operation in Matlab @@ -61,7 +76,7 @@ def test_line_magic(self): # Get the result back to Python self.ip.run_cell_magic('matlab', '-o actual', 'actual = res') - self.ip.run_cell("expected = np.array([2, 4, 6])") + self.ip.run_cell("expected = np.array([[2., 4., 6.]])") npt.assert_almost_equal(self.ip.user_ns['actual'], self.ip.user_ns['expected'], decimal=7) @@ -87,5 +102,9 @@ def test_struct(self): 'obj.num = num;obj.num_array = num_array;obj.str = str;') npt.assert_equal(isinstance(self.ip.user_ns['obj'], dict), True) npt.assert_equal(self.ip.user_ns['obj']['num'], self.ip.user_ns['num']) - npt.assert_equal(self.ip.user_ns['obj']['num_array'], self.ip.user_ns['num_array']) + npt.assert_equal(self.ip.user_ns['obj']['num_array'].squeeze(), self.ip.user_ns['num_array']) npt.assert_equal(self.ip.user_ns['obj']['str'], self.ip.user_ns['str']) + + def test_faulty(self): + npt.assert_raises(MatlabInterperterError, + lambda: self.ip.run_line_magic('matlab', '1 = 2')) diff --git a/pymatbridge/tests/test_publish.m b/pymatbridge/tests/test_publish.m new file mode 100644 index 0000000..2590b0c --- /dev/null +++ b/pymatbridge/tests/test_publish.m @@ -0,0 +1,14 @@ +%% Experimenting with conversion from matlab to ipynb +% This is a second comment line within the header block +% Next, I will put in an empty line (new cell?) and then some code + +% This is some code: +t = linspace(1, 6*pi, 100); +a = sin(t); +plot(a) +grid on + +%% A new cell +b = cos(t); +plot(b); +b(1:10) % What happens if you print to the command line? \ No newline at end of file diff --git a/pymatbridge/tests/test_publish.py b/pymatbridge/tests/test_publish.py new file mode 100644 index 0000000..cb3f093 --- /dev/null +++ b/pymatbridge/tests/test_publish.py @@ -0,0 +1,59 @@ +import numpy.testing as npt +import pymatbridge.publish as publish +import json +import os + + +MFILE = os.path.join(os.path.dirname(__file__), 'test_publish.m') + + +def test_format_line(): + """ + Test that lines get formatted properly + + """ + npt.assert_equal(publish.format_line('%% This begins a new cell'), + (True, True, ' This begins a new cell\n')) + + npt.assert_equal(publish.format_line('% This should be just markdown'), + (False, True, ' This should be just markdown\n')) + + npt.assert_equal(publish.format_line('This is just code'), + (False, False, 'This is just code')) + + +def test_lines_to_notebook(): + """ + Test that conversion of some lines gives you a proper notebook + """ + + lines = ["%% This is a first line\n", + "\n", + "% This should be in another cell\n", + "a = 1; % This is code\n", + "b = 2; % This is also code\n", + "c = 3; % code in another cell\n"] + + nb = publish.lines_to_notebook(lines) + + npt.assert_equal(nb['cells'][1]['source'][0], + ' This is a first line\n\n') + + +def test_convert_mfile(): + publish.convert_mfile(MFILE) + nb_file = MFILE.replace('.m', '.ipynb') + with open(nb_file) as fid: + nb = json.load(fid) + npt.assert_equal(nb['cells'][1]['source'][0], + ' Experimenting with conversion from matlab to ipynb\n\n') + os.remove(nb_file) + + +def test_mfile_to_lines(): + lines = publish.mfile_to_lines(MFILE) + + nb = publish.lines_to_notebook(lines) + + npt.assert_equal(nb['cells'][1]['source'][0], + ' Experimenting with conversion from matlab to ipynb\n\n') diff --git a/pymatbridge/tests/test_run_code.py b/pymatbridge/tests/test_run_code.py index 45b2ae8..e4255e7 100644 --- a/pymatbridge/tests/test_run_code.py +++ b/pymatbridge/tests/test_run_code.py @@ -1,4 +1,8 @@ +import os + import pymatbridge as pymat +from pymatbridge.compat import text_type +import numpy as np import numpy.testing as npt import test_utils as tu @@ -23,7 +27,10 @@ def test_disp(self): npt.assert_equal(result1, "Hello world\n") npt.assert_equal(result2, " \n") - npt.assert_equal(result3, "") + if tu.on_octave(): + npt.assert_equal(result3, '\n') + else: + npt.assert_equal(result3, "") # Make some assignments and run basic operations def test_basic_operation(self): @@ -34,18 +41,44 @@ def test_basic_operation(self): result_product = self.mlab.run_code("a * b")['content']['stdout'] result_division = self.mlab.run_code("c = a / b")['content']['stdout'] - - npt.assert_equal(result_assignment_a, unicode("\na =\n\n 21.2345\n\n")) - npt.assert_equal(result_assignment_b, unicode("\nb =\n\n 347.7450\n\n")) - npt.assert_equal(result_sum, unicode("\nans =\n\n 368.9795\n\n")) - npt.assert_equal(result_diff, unicode("\nans =\n\n -326.5105\n\n")) - npt.assert_equal(result_product, unicode("\nans =\n\n 7.3842e+03\n\n")) - npt.assert_equal(result_division, unicode("\nc =\n\n 0.0611\n\n")) + if tu.on_octave(): + npt.assert_equal(result_assignment_a, text_type("a = 21.235\n")) + npt.assert_equal(result_assignment_b, text_type("b = 347.75\n")) + npt.assert_equal(result_sum, text_type("ans = 368.98\n")) + npt.assert_equal(result_diff, text_type("ans = -326.51\n")) + npt.assert_equal(result_product, text_type("ans = 7384.2\n")) + npt.assert_equal(result_division, text_type("c = 0.061063\n")) + else: + npt.assert_equal(result_assignment_a, text_type("\na =\n\n 21.2345\n\n")) + npt.assert_equal(result_assignment_b, text_type("\nb =\n\n 347.7450\n\n")) + npt.assert_equal(result_sum, text_type("\nans =\n\n 368.9795\n\n")) + npt.assert_equal(result_diff, text_type("\nans =\n\n -326.5105\n\n")) + npt.assert_equal(result_product, text_type("\nans =\n\n 7.3842e+03\n\n")) + npt.assert_equal(result_division, text_type("\nc =\n\n 0.0611\n\n")) # Put in some undefined code def test_undefined_code(self): success = self.mlab.run_code("this_is_nonsense")['success'] message = self.mlab.run_code("this_is_nonsense")['content']['stdout'] - npt.assert_equal(success, "false") - npt.assert_equal(message, "Undefined function or variable 'this_is_nonsense'.") + assert not success + if tu.on_octave(): + 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_stack_traces(self): + this_dir = os.path.abspath(os.path.dirname(__file__)) + test_file = os.path.join(this_dir, 'test_stack_trace.m') + + self.mlab.run_code("addpath('%s')" % this_dir) + response = self.mlab.run_code('test_stack_trace(10)') + npt.assert_equal(response['stack'], [ + {'name': 'baz', 'line': 14, 'file': test_file}, + {'name': 'bar', 'line': 10, 'file': test_file}, + {'name': 'foo', 'line': 6, 'file': test_file}, + {'name': 'test_stack_trace', 'line': 2, 'file': test_file} + ]) + + response = self.mlab.run_code('x = 2') + npt.assert_equal(response['stack'], []) diff --git a/pymatbridge/tests/test_set_variable.py b/pymatbridge/tests/test_set_variable.py new file mode 100644 index 0000000..fb44624 --- /dev/null +++ b/pymatbridge/tests/test_set_variable.py @@ -0,0 +1,49 @@ +import pymatbridge as pymat +import random as rd +import numpy as np +import numpy.testing as npt +import test_utils as tu + +class TestArray: + + # Start a Matlab session before any tests + @classmethod + def setup_class(cls): + cls.mlab = tu.connect_to_matlab() + + # Tear down the Matlab session after all the tests are done + @classmethod + def teardown_class(cls): + tu.stop_matlab(cls.mlab) + + + # Pass a 1000*1000 array to Matlab + def test_array_size(self): + array = np.random.random_sample((50,50)) + res = self.mlab.run_func("array_size.m",{'val':array})['result'] + npt.assert_almost_equal(res, array, decimal=8, err_msg = "test_array_size: error") + + + def test_array_content(self): + test_array = np.random.random_integers(2, 20, (5, 10)) + self.mlab.set_variable('test', test_array) + npt.assert_equal(self.mlab.get_variable('test'), test_array) + test_array = np.asfortranarray(test_array) + self.mlab.set_variable('test', test_array) + npt.assert_equal(self.mlab.get_variable('test'), test_array) + # force non-contiguous + test_array = test_array[::-1] + self.mlab.set_variable('test', test_array) + npt.assert_equal(self.mlab.get_variable('test'), test_array) + + def test_object_array(self): + test_array = np.array(['hello', 1]) + self.mlab.set_variable('test', test_array) + npt.assert_equal(self.mlab.get_variable('test'), test_array) + + def test_others(self): + self.mlab.set_variable('test', np.float(1.5)) + npt.assert_equal(self.mlab.get_variable('test'), 1.5) + self.mlab.set_variable('test', 'hello') + npt.assert_equal(self.mlab.get_variable('test'), 'hello') + diff --git a/pymatbridge/tests/test_stack_trace.m b/pymatbridge/tests/test_stack_trace.m new file mode 100644 index 0000000..0878c2c --- /dev/null +++ b/pymatbridge/tests/test_stack_trace.m @@ -0,0 +1,15 @@ +function o = test_stack_trace(input) + o = foo(input); +end + +function o = foo(input) + o = bar(input) +end + +function o = bar(input) + o = baz(input) +end + +function o = baz(input) + o = quux(input) +end diff --git a/pymatbridge/tests/test_utils.py b/pymatbridge/tests/test_utils.py index 0d8131e..ecb7add 100644 --- a/pymatbridge/tests/test_utils.py +++ b/pymatbridge/tests/test_utils.py @@ -1,8 +1,12 @@ +import os import pymatbridge as pymat import numpy.testing as npt +def on_octave(): + return bool(os.environ.get('USE_OCTAVE', False)) + def connect_to_matlab(): - mlab = pymat.Matlab() + mlab = pymat.Octave() if on_octave() else pymat.Matlab(log=True) mlab.start() npt.assert_(mlab.is_connected(), msg = "connect_to_matlab(): Connection failed") @@ -13,3 +17,6 @@ def stop_matlab(mlab): mlab.stop() npt.assert_(not mlab.is_connected(), msg = "stop_matlab(): Disconnection failed") + + + diff --git a/pymatbridge/version.py b/pymatbridge/version.py index bd511ce..ad5ff15 100644 --- a/pymatbridge/version.py +++ b/pymatbridge/version.py @@ -2,8 +2,8 @@ # Format expected by setup.py and doc/source/conf.py: string of form "X.Y.Z" _version_major = 0 -_version_minor = 3 -_version_micro = '' # use '' for first of series, number for 1 and above +_version_minor = 6 +_version_micro = 0 #'' # use '' for first of series, number for 1 and above _version_extra = 'dev' #_version_extra = '' # Uncomment this for full releases @@ -31,11 +31,8 @@ Pymatbridge =========== -This package provides a set of python and matlab functions with the goal of -providing an easy, seamless way to call Matlab functions within Python (not so -much the other way, because why would anyone want to do something like that?). +A python interface to call out to Matlab(R). -TODO: more documentation will be here at some point. License information =================== @@ -58,15 +55,7 @@ software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE - FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ @@ -85,14 +74,24 @@ MINOR = _version_minor MICRO = _version_micro VERSION = __version__ -PACKAGES = ['pymatbridge'] +PACKAGES = ['pymatbridge', + 'pymatbridge.messenger'] PACKAGE_DATA = {"pymatbridge": ["matlab/matlabserver.m", "matlab/messenger.*", "matlab/usrprog/*", "matlab/util/*.m", "matlab/util/json_v0.2.2/LICENSE", "matlab/util/json_v0.2.2/README.md", "matlab/util/json_v0.2.2/test/*", - "matlab/util/json_v0.2.2/+json/*.m", - "matlab/util/json_v0.2.2/+json/java/*", - "tests/*.py", "examples/*.ipynb"]} - -REQUIRES = [] + "matlab/util/json_v0.2.2/json/*.m", + "matlab/util/json_v0.2.2/json/java/*", + "tests/*.py", "tests/*.m", "examples/*.ipynb"], + "pymatbridge.messenger": ["mexmaci64/*", + "mexw64/*", + "mexa64/*"]} + +REQUIRES = ['pyzmq'] +EXTRAS_REQUIRE = { + 'sparse arrays': ["scipy>=0.13.0"], + 'ipython': ["ipython>=3.0"], +} + +BIN=['scripts/publish-notebook'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ae3bcc7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyzmq +numpy>=1.7 diff --git a/scripts/publish-notebook b/scripts/publish-notebook new file mode 100755 index 0000000..14e57c5 --- /dev/null +++ b/scripts/publish-notebook @@ -0,0 +1,17 @@ +#!/usr/bin/env python +import argparse as arg +import pymatbridge.publish as publish + +parser = arg.ArgumentParser(description='Publish a matlab file (.m) as an interactive notebook (.ipynb)') + +parser.add_argument('mfile', action='store', metavar='File', + help='Matlab m-file (.m)') + +parser.add_argument('--outfile', action='store', metavar='File', + help='Output notebook (.ipynb). Default: same name and location as the input file ', default=None) + +params = parser.parse_args() + + +if __name__ == "__main__": + publish.convert_mfile(params.mfile, params.outfile) diff --git a/setup.py b/setup.py index bc27f56..766aaae 100755 --- a/setup.py +++ b/setup.py @@ -2,41 +2,15 @@ """Setup file for python-matlab-bridge""" import os -import sys -import shutil -# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly -# update it when the contents of directories change. -if os.path.exists('MANIFEST'): - os.remove('MANIFEST') - -from distutils.core import setup - -# Find the messenger binary file and copy it to /matlab folder. -bin_location = "" -if sys.platform == "darwin": - if not os.path.exists("./messenger/mexmaci64/messenger.mexmaci64"): - raise ValueError("messenger.mexmaci64 is not built yet. Please build it yourself.") - bin_location = "./messenger/mexmaci64/messenger.mexmaci64" - -elif sys.platform == "linux2": - if not os.path.exists("./messenger/mexa64/messenger.mexa64"): - raise ValueError("messenger.mexa64 is not built yet. Please build it yourself.") - bin_location = "./messenger/mexa64/messenger.mexa64" - -elif sys.platform == "win32": - if not os.path.exists("./messenger/mexw32/messenger.mexw32"): - raise ValueError("messenger.mexw32 is not built yet. Please build it yourself.") - bin_location = "./messenger/mexw32/messenger.mexw32" - -else: - raise ValueError("Known platform") - -shutil.copy(bin_location, "./pymatbridge/matlab") +try: + from setuptools import setup +except ImportError: + from distutils.core import setup # Get version and release info, which is all stored in pymatbridge/version.py ver_file = os.path.join('pymatbridge', 'version.py') -execfile(ver_file) +exec(open(ver_file).read()) opts = dict(name=NAME, maintainer=MAINTAINER, @@ -54,18 +28,11 @@ packages=PACKAGES, package_data=PACKAGE_DATA, requires=REQUIRES, + extras_require=EXTRAS_REQUIRE, + scripts=BIN, + install_requires=['pyzmq', 'numpy'] ) -# For some commands, use setuptools. Note that we do NOT list install here! -# If you want a setuptools-enhanced install, just run 'setupegg.py install' -needs_setuptools = set(('develop', )) -if len(needs_setuptools.intersection(sys.argv)) > 0: - import setuptools - -# Only add setuptools-specific flags if the user called for setuptools, but -# otherwise leave it alone -if 'setuptools' in sys.modules: - opts['zip_safe'] = False # Now call the actual setup function if __name__ == '__main__': diff --git a/tools/gh_api.py b/tools/gh_api.py new file mode 100644 index 0000000..8c62098 --- /dev/null +++ b/tools/gh_api.py @@ -0,0 +1,292 @@ +"""Functions for Github API requests.""" +from __future__ import print_function + +try: + input = raw_input +except NameError: + pass + +import os +import re +import sys + +import requests +import getpass +import json + +try: + import requests_cache +except ImportError: + print("no cache", file=sys.stderr) +else: + requests_cache.install_cache("gh_api", expire_after=3600) + +# Keyring stores passwords by a 'username', but we're not storing a username and +# password +fake_username = 'ipython_tools' + +class Obj(dict): + """Dictionary with attribute access to names.""" + def __getattr__(self, name): + try: + return self[name] + except KeyError: + raise AttributeError(name) + + def __setattr__(self, name, val): + self[name] = val + +token = None +def get_auth_token(): + global token + + if token is not None: + return token + + import keyring + token = keyring.get_password('github', fake_username) + if token is not None: + return token + + print("Please enter your github username and password. These are not " + "stored, only used to get an oAuth token. You can revoke this at " + "any time on Github.") + user = input("Username: ") + pw = getpass.getpass("Password: ") + + auth_request = { + "scopes": [ + "public_repo", + "gist" + ], + "note": "IPython tools", + "note_url": "https://github.com/ipython/ipython/tree/master/tools", + } + response = requests.post('https://api.github.com/authorizations', + auth=(user, pw), data=json.dumps(auth_request)) + response.raise_for_status() + token = json.loads(response.text)['token'] + keyring.set_password('github', fake_username, token) + return token + +def make_auth_header(): + return {'Authorization': 'token ' + get_auth_token()} + +def post_issue_comment(project, num, body): + url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num) + payload = json.dumps({'body': body}) + requests.post(url, data=payload, headers=make_auth_header()) + +def post_gist(content, description='', filename='file', auth=False): + """Post some text to a Gist, and return the URL.""" + post_data = json.dumps({ + "description": description, + "public": True, + "files": { + filename: { + "content": content + } + } + }).encode('utf-8') + + headers = make_auth_header() if auth else {} + response = requests.post("https://api.github.com/gists", data=post_data, headers=headers) + response.raise_for_status() + response_data = json.loads(response.text) + return response_data['html_url'] + +def get_pull_request(project, num, auth=False): + """get pull request info by number + """ + url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num) + if auth: + header = make_auth_header() + else: + header = None + response = requests.get(url, headers=header) + response.raise_for_status() + return json.loads(response.text, object_hook=Obj) + +def get_pull_request_files(project, num, auth=False): + """get list of files in a pull request""" + url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num) + if auth: + header = make_auth_header() + else: + header = None + return get_paged_request(url, headers=header) + +element_pat = re.compile(r'<(.+?)>') +rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]') + +def get_paged_request(url, headers=None, **params): + """get a full list, handling APIv3's paging""" + results = [] + params.setdefault("per_page", 100) + while True: + if '?' in url: + params = None + print("fetching %s" % url, file=sys.stderr) + else: + print("fetching %s with %s" % (url, params), file=sys.stderr) + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + results.extend(response.json()) + if 'next' in response.links: + url = response.links['next']['url'] + else: + break + return results + +def get_pulls_list(project, auth=False, **params): + """get pull request list""" + params.setdefault("state", "closed") + url = "https://api.github.com/repos/{project}/pulls".format(project=project) + if auth: + headers = make_auth_header() + else: + headers = None + pages = get_paged_request(url, headers=headers, **params) + return pages + +def get_issues_list(project, auth=False, **params): + """get issues list""" + params.setdefault("state", "closed") + url = "https://api.github.com/repos/{project}/issues".format(project=project) + if auth: + headers = make_auth_header() + else: + headers = None + pages = get_paged_request(url, headers=headers, **params) + return pages + +def get_milestones(project, auth=False, **params): + url = "https://api.github.com/repos/{project}/milestones".format(project=project) + if auth: + headers = make_auth_header() + else: + headers = None + milestones = get_paged_request(url, headers=headers, **params) + return milestones + +def get_milestone_id(project, milestone, auth=False, **params): + milestones = get_milestones(project, auth=auth, **params) + for mstone in milestones: + if mstone['title'] == milestone: + return mstone['number'] + else: + raise ValueError("milestone %s not found" % milestone) + +def is_pull_request(issue): + """Return True if the given issue is a pull request.""" + return bool(issue.get('pull_request', {}).get('html_url', None)) + +def get_authors(pr): + print("getting authors for #%i" % pr['number'], file=sys.stderr) + h = make_auth_header() + r = requests.get(pr['commits_url'], headers=h) + r.raise_for_status() + commits = r.json() + authors = [] + for commit in commits: + author = commit['commit']['author'] + authors.append("%s <%s>" % (author['name'], author['email'])) + return authors + +# encode_multipart_formdata is from urllib3.filepost +# The only change is to iter_fields, to enforce S3's required key ordering + +def iter_fields(fields): + fields = fields.copy() + for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId', + 'Policy', 'Signature', 'Content-Type', 'file'): + yield (key, fields.pop(key)) + for (k,v) in fields.items(): + yield k,v + +def encode_multipart_formdata(fields, boundary=None): + """ + Encode a dictionary of ``fields`` using the multipart/form-data mime format. + + :param fields: + Dictionary of fields or list of (key, value) field tuples. The key is + treated as the field name, and the value as the body of the form-data + bytes. If the value is a tuple of two elements, then the first element + is treated as the filename of the form-data section. + + Field names and filenames must be unicode. + + :param boundary: + If not specified, then a random boundary will be generated using + :func:`mimetools.choose_boundary`. + """ + # copy requests imports in here: + from io import BytesIO + from requests.packages.urllib3.filepost import ( + choose_boundary, six, writer, b, get_content_type + ) + body = BytesIO() + if boundary is None: + boundary = choose_boundary() + + for fieldname, value in iter_fields(fields): + body.write(b('--%s\r\n' % (boundary))) + + if isinstance(value, tuple): + filename, data = value + writer(body).write('Content-Disposition: form-data; name="%s"; ' + 'filename="%s"\r\n' % (fieldname, filename)) + body.write(b('Content-Type: %s\r\n\r\n' % + (get_content_type(filename)))) + else: + data = value + writer(body).write('Content-Disposition: form-data; name="%s"\r\n' + % (fieldname)) + body.write(b'Content-Type: text/plain\r\n\r\n') + + if isinstance(data, int): + data = str(data) # Backwards compatibility + if isinstance(data, six.text_type): + writer(body).write(data) + else: + body.write(data) + + body.write(b'\r\n') + + body.write(b('--%s--\r\n' % (boundary))) + + content_type = b('multipart/form-data; boundary=%s' % boundary) + + return body.getvalue(), content_type + + +def post_download(project, filename, name=None, description=""): + """Upload a file to the GitHub downloads area""" + if name is None: + name = os.path.basename(filename) + with open(filename, 'rb') as f: + filedata = f.read() + + url = "https://api.github.com/repos/{project}/downloads".format(project=project) + + payload = json.dumps(dict(name=name, size=len(filedata), + description=description)) + response = requests.post(url, data=payload, headers=make_auth_header()) + response.raise_for_status() + reply = json.loads(response.content) + s3_url = reply['s3_url'] + + fields = dict( + key=reply['path'], + acl=reply['acl'], + success_action_status=201, + Filename=reply['name'], + AWSAccessKeyId=reply['accesskeyid'], + Policy=reply['policy'], + Signature=reply['signature'], + file=(reply['name'], filedata), + ) + fields['Content-Type'] = reply['mime_type'] + data, content_type = encode_multipart_formdata(fields) + s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type}) + return s3r diff --git a/tools/github_stats.py b/tools/github_stats.py new file mode 100755 index 0000000..572043d --- /dev/null +++ b/tools/github_stats.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python +"""Simple tools to query github.com and gather stats about issues. + +Thanks to the IPython team for developing this! + + python github_stats.py --milestone 2.0 --since-tag rel-1.0.0 +""" +#----------------------------------------------------------------------------- +# Imports +#----------------------------------------------------------------------------- + +from __future__ import print_function + +import codecs +import sys + +from argparse import ArgumentParser +from datetime import datetime, timedelta +from subprocess import check_output + +from gh_api import ( + get_paged_request, make_auth_header, get_pull_request, is_pull_request, + get_milestone_id, get_issues_list, get_authors, +) +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +ISO8601 = "%Y-%m-%dT%H:%M:%SZ" +PER_PAGE = 100 + +#----------------------------------------------------------------------------- +# Functions +#----------------------------------------------------------------------------- + +def round_hour(dt): + return dt.replace(minute=0,second=0,microsecond=0) + +def _parse_datetime(s): + """Parse dates in the format returned by the Github API.""" + if s: + return datetime.strptime(s, ISO8601) + else: + return datetime.fromtimestamp(0) + +def issues2dict(issues): + """Convert a list of issues to a dict, keyed by issue number.""" + idict = {} + for i in issues: + idict[i['number']] = i + return idict + +def split_pulls(all_issues, project="arokem/python-matlab-bridge"): + """split a list of closed issues into non-PR Issues and Pull Requests""" + pulls = [] + issues = [] + for i in all_issues: + if is_pull_request(i): + pull = get_pull_request(project, i['number'], auth=True) + pulls.append(pull) + else: + issues.append(i) + return issues, pulls + + +def issues_closed_since(period=timedelta(days=365), project="arokem/python-matlab-bridge", pulls=False): + """Get all issues closed since a particular point in time. period + can either be a datetime object, or a timedelta object. In the + latter case, it is used as a time before the present. + """ + + which = 'pulls' if pulls else 'issues' + + if isinstance(period, timedelta): + since = round_hour(datetime.utcnow() - period) + else: + since = period + url = "https://api.github.com/repos/%s/%s?state=closed&sort=updated&since=%s&per_page=%i" % (project, which, since.strftime(ISO8601), PER_PAGE) + allclosed = get_paged_request(url, headers=make_auth_header()) + + filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ] + if pulls: + filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] + # filter out PRs not against master (backports) + filtered = [ i for i in filtered if i['base']['ref'] == 'master' ] + else: + filtered = [ i for i in filtered if not is_pull_request(i) ] + + return filtered + + +def sorted_by_field(issues, field='closed_at', reverse=False): + """Return a list of issues sorted by closing date date.""" + return sorted(issues, key = lambda i:i[field], reverse=reverse) + + +def report(issues, show_urls=False): + """Summary report about a list of issues, printing number and title. + """ + # titles may have unicode in them, so we must encode everything below + if show_urls: + for i in issues: + print(u'#%d: %s' % (i['number'], + i['title'].replace(u'`', u'``'))) + else: + for i in issues: + print(u'* %d: %s' % (i['number'], i['title'].replace(u'`', u'``'))) + +#----------------------------------------------------------------------------- +# Main script +#----------------------------------------------------------------------------- + +if __name__ == "__main__": + # deal with unicode + sys.stdout = codecs.getwriter('utf8')(sys.stdout) + + # Whether to add reST urls for all issues in printout. + show_urls = True + + parser = ArgumentParser() + parser.add_argument('--since-tag', type=str, + help="The git tag to use for the starting point (typically the last major release)." + ) + parser.add_argument('--milestone', type=str, + help="The GitHub milestone to use for filtering issues [optional]." + ) + parser.add_argument('--days', type=int, + help="The number of days of data to summarize (use this or --since-tag)." + ) + parser.add_argument('--project', type=str, default="arokem/python-matlab-bridge", + help="The project to summarize." + ) + + opts = parser.parse_args() + tag = opts.since_tag + + # set `since` from days or git tag + if opts.days: + since = datetime.utcnow() - timedelta(days=opts.days) + else: + if not tag: + tag = check_output(['git', 'describe', '--abbrev=0']).strip() + cmd = ['git', 'log', '-1', '--format=%ai', tag] + tagday, tz = check_output(cmd).strip().rsplit(' ', 1) + since = datetime.strptime(tagday, "%Y-%m-%d %H:%M:%S") + h = int(tz[1:3]) + m = int(tz[3:]) + td = timedelta(hours=h, minutes=m) + if tz[0] == '-': + since += td + else: + since -= td + + since = round_hour(since) + + milestone = opts.milestone + project = opts.project + + print("fetching GitHub stats since %s (tag: %s, milestone: %s)" % (since, tag, milestone), file=sys.stderr) + if milestone: + milestone_id = get_milestone_id(project=project, milestone=milestone, + auth=True) + issues_and_pulls = get_issues_list(project=project, + milestone=milestone_id, + state='closed', + auth=True, + ) + issues, pulls = split_pulls(issues_and_pulls) + else: + issues = issues_closed_since(since, project=project, pulls=False) + pulls = issues_closed_since(since, project=project, pulls=True) + + # For regular reports, it's nice to show them in reverse chronological order + issues = sorted_by_field(issues, reverse=True) + pulls = sorted_by_field(pulls, reverse=True) + + n_issues, n_pulls = map(len, (issues, pulls)) + n_total = n_issues + n_pulls + + # Print summary report we can directly include into release notes. + + print() + since_day = since.strftime("%Y/%m/%d") + today = datetime.today().strftime("%Y/%m/%d") + print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag)) + print() + print("These lists are automatically generated, and may be incomplete or contain duplicates.") + print() + + ncommits = 0 + all_authors = [] + if tag: + # print git info, in addition to GitHub info: + since_tag = tag+'..' + cmd = ['git', 'log', '--oneline', since_tag] + ncommits += len(check_output(cmd).splitlines()) + + author_cmd = ['git', 'log', '--use-mailmap', "--format=* %aN", since_tag] + all_authors.extend(check_output(author_cmd).decode('utf-8', 'replace').splitlines()) + + pr_authors = [] + for pr in pulls: + pr_authors.extend(get_authors(pr)) + ncommits = len(pr_authors) + ncommits - len(pulls) + author_cmd = ['git', 'check-mailmap'] + pr_authors + with_email = check_output(author_cmd).decode('utf-8', 'replace').splitlines() + all_authors.extend([ u'* ' + a.split(' <')[0] for a in with_email ]) + unique_authors = sorted(set(all_authors), key=lambda s: s.lower()) + + print("The following %i authors contributed %i commits." % (len(unique_authors), ncommits)) + print() + print('\n'.join(unique_authors)) + + print() + print("We closed %d issues and merged %d pull requests;\n" + "this is the full list (generated with the script \n" + ":file:`tools/github_stats.py`):" % (n_pulls, n_issues)) + print() + print('Pull Requests (%d):\n' % n_pulls) + report(pulls, show_urls) + print() + print('Issues (%d):\n' % n_issues) + report(issues, show_urls)