diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e9bfe4c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +matlab_kernel/_version.py export-subst diff --git a/.gitignore b/.gitignore index 7de92c5..6d29828 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ +*.egg-info/ +.cache/ .ipynb_checkpoints/ -MANIFEST -dist/ build/ -*.egg-info/ -__pycache__ +dist/ *.pyc +.*.swp +matlab_kernel/matlab/ +matlab_kernel/matlab_connect/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..426f99b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: python +python: + - "2.7" + - "3.5" +install: + - pip install . +script: + - make test + - python -m matlab_kernel install --user + - python -m matlab_kernel.check diff --git a/HISTORY.rst b/HISTORY.rst deleted file mode 100644 index c1b96d5..0000000 --- a/HISTORY.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. :changelog: - -Release History ---------------- - -0.8 (2016-01-17) -++++++++++++++++ -- Fix startup and add support for Octave. - - -0.7 (2016-01-14) -++++++++++++++++ -- Switch to 2-step install. - - -0.4 (2015-02-27) -+++++++++++++++++ -- Fix first execution command -- Fix restart handling - stop current matlab process - - -0.3 (2015-02-10) -+++++++++++++++++ -- Add support for %plot magic (size) and saner default size - - -0.1 (2015-02-01) -++++++++++++++++++ -- Initial release diff --git a/LICENSE.txt b/LICENSE.txt index 97f1d5a..576b967 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2016, Steven Silvester +Copyright (c) 2016, Steven Silvester, Antony Lee All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -10,3 +10,28 @@ Redistribution and use in source and binary forms, with or without modification, 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this 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. + + +wurlitzer was originally published under the following license: + +The MIT License (MIT) + +Copyright (c) 2016 Min RK + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2e162d9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include versioneer.py +include matlab_kernel/_version.py +include matlab_kernel/kernel_template.json +recursive-include matlab_kernel *.png diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ee707b6 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +# Note: This is meant for octave_kernel developer use only +.PHONY: all clean test release + +export NAME=`python setup.py --name 2>/dev/null` +export VERSION=`python setup.py --version 2>/dev/null` + +all: clean + pip install . + +clean: + rm -rf build + rm -rf dist + +test: clean + pip install . + python -c "from jupyter_client.kernelspec import find_kernel_specs; assert 'matlab' in find_kernel_specs()" + +release: test clean + pip install wheel + git commit -a -m "Release $(VERSION)"; true + git tag v$(VERSION) + rm -rf dist + python setup.py register + python setup.py bdist_wheel --universal + python setup.py sdist + git push origin --all + git push origin --tags + twine upload dist/* diff --git a/README.rst b/README.rst index 60991cd..a36a4cd 100644 --- a/README.rst +++ b/README.rst @@ -1,41 +1,109 @@ -A Jupyter/IPython kernel for Matlab +A Matlab kernel for Jupyter +=========================== -This requires `Jupyter Notebook `_, -and either `pymatbridge `_, or the +Prerequisites +------------- +Install `Jupyter Notebook `_ and the `Matlab engine for Python `_. -To install:: +Installation +------------ - pip install matlab_kernel - python -m matlab_kernel install +Install using:: -To use it, run one of: + $ pip install matlab_kernel -.. code:: shell +or ``pip install git+https://github.com/Calysto/matlab_kernel`` for the dev version. + +To use the kernel, run one of:: - ipython notebook + $ jupyter notebook # In the notebook interface, select Matlab from the 'New' menu - ipython qtconsole --kernel matlab - ipython console --kernel matlab + $ jupyter qtconsole --kernel matlab + $ jupyter console --kernel matlab -This is based on `MetaKernel `_, -which means it features a standard set of magics. +To remove from kernel listings:: -A sample notebook is available online_. + $ jupyter kernelspec remove matlab + + +Configuration +------------- +The kernel can be configured by adding an ``matlab_kernel_config.py`` file to the +``jupyter`` config path. The ``MatlabKernel`` class offers ``plot_settings`` as a configurable traits. +The available plot settings are: +'format', 'backend', 'width', 'height', and 'resolution'. + +.. code:: bash + + cat ~/.jupyter/matlab_kernel_config.py + c.MatlabKernel.plot_settings = dict(format='svg') + + +Troubleshooting +--------------- + +Kernel Times Out While Starting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If the kernel is not starting, try running the following from a terminal. + +.. code:: shell + + python -m matlab_kernel.check + +Please include that output if opening an issue. + + +Kernel is Not Listed +~~~~~~~~~~~~~~~~~~~~ +If the kernel is not listed as an available kernel, first try the following command: + +.. code:: shell + + python -m matlab_kernel install --user + +If the kernel is still not listed, verify that the following point to the same +version of python: + +.. code:: shell -If using `pymatbridge`, you can specify the path to your Matlab executable by -creating a `MATLAB_EXECUTABLE` environment variable:: + which python # use "where" if using cmd.exe + which jupyter - MATLAB_EXECUTABLE=/usr/bin/matlab - ipython notebook --kernel=matlab_kernel -For example, on OSX, you could add something like the following to ~/.bash_profile:: +Additional information +---------------------- - export MATLAB_EXECUTABLE=/Applications/MATLAB_2015b.app/bin/matlab +The Matlab kernel is based on `MetaKernel `_, +which means it features a standard set of magics. For a full list of magics, +run ``%lsmagic`` in a cell. + +A sample notebook is available online_. A note about plotting. After each call to Matlab, we ask Matlab to save any -open figures to image files whose format and resolution are defined using -the `%plot` magic. The resulting image is shown inline in the notebook. +open figures to image files whose format and resolution are defined using the +``%plot`` magic. The resulting image is shown inline in the notebook. You can +use ``%plot native`` to raise normal Matlab windows instead. + + +Advanced Installation Notes +--------------------------- + +We automatically install a Jupyter kernelspec when installing the python package. This location can be found using ``jupyter kernelspec list``. If the default location is not desired, you can remove the directory for the octave kernel, and install using ``python -m matlab_kernel install``. See ``python -m matlab_kernel install --help`` for available options. + +It has been reported that Matlab version 2016b works fine. However, Matlab 2014b does not work with Python 3.5. .. _online: http://nbviewer.ipython.org/github/Calysto/matlab_kernel/blob/master/matlab_kernel.ipynb + + +Development +~~~~~~~~~~~ + +Install the package locally:: + + $ pip install -e . + $ python -m matlab_kernel install + +As you make changes, test them in a notebook (restart the kernel between changes). + diff --git a/flit.ini b/flit.ini deleted file mode 100644 index 163a497..0000000 --- a/flit.ini +++ /dev/null @@ -1,13 +0,0 @@ -[metadata] -module = matlab_kernel -author = Steven Silvester -author-email = steven.silvester@ieee.org -home-page = https://github.com/calysto/matlab_kernel -requires = metakernel (>=0.13.1) -dev-requires = jupyter_kernel_test -description-file = README.rst -classifiers = Framework :: IPython - License :: OSI Approved :: BSD License - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Topic :: System :: Shells diff --git a/matlab_kernel.ipynb b/matlab_kernel.ipynb index e43b8f3..be7e02a 100644 --- a/matlab_kernel.ipynb +++ b/matlab_kernel.ipynb @@ -7,7 +7,7 @@ "Jupyter Matlab Kernel\n", "============\n", "\n", - "Interact with Matlab in Notebook the using the [python-matlab-bridge](https://pypi.python.org/pypi/pymatbridge/0.4.3). All commands are interpreted by Matlab. Since this is a [MetaKernel](https://github.com/Calysto/metakernel), a standard set of magics are available. Help on commands is available using the `%help` magic or using `?` with a command." + "Interact with Matlab in Notebook the using the [Matlab engine for Python](https://www.mathworks.com/help/matlab/matlab-engine-for-python.html). All commands are interpreted by Matlab. Since this is a [MetaKernel](https://github.com/Calysto/metakernel), a standard set of magics are available. Help on commands is available using the `%help` magic or using `?` with a command." ] }, { @@ -643,7 +643,7 @@ "kernelspec": { "display_name": "Matlab", "language": "matlab", - "name": "matlab_kernel" + "name": "matlab" }, "language_info": { "file_extension": ".m", diff --git a/matlab_kernel/__init__.py b/matlab_kernel/__init__.py index 3c48a54..eeebea6 100644 --- a/matlab_kernel/__init__.py +++ b/matlab_kernel/__init__.py @@ -1,3 +1,3 @@ """A Matlab kernel for Jupyter""" -__version__ = '0.11.0' +__version__ = '0.17.1' diff --git a/matlab_kernel/check.py b/matlab_kernel/check.py new file mode 100644 index 0000000..aea0722 --- /dev/null +++ b/matlab_kernel/check.py @@ -0,0 +1,19 @@ +import sys +from metakernel import __version__ as mversion +from . import __version__ +from .kernel import MatlabKernel + + +if __name__ == "__main__": + print('Matlab kernel v%s' % __version__) + print('Metakernel v%s' % mversion) + print('Python v%s' % sys.version) + print('Python path: %s' % sys.executable) + print('\nConnecting to Matlab...') + try: + m = MatlabKernel() + print('Matlab connection established') + print(m.banner) + print(m.do_execute_direct('disp("hi from Matlab!")')) + except Exception as e: + print(e) diff --git a/matlab_kernel/kernel.py b/matlab_kernel/kernel.py index 4bff2e7..7065109 100644 --- a/matlab_kernel/kernel.py +++ b/matlab_kernel/kernel.py @@ -1,216 +1,295 @@ -from __future__ import print_function - -import os -import sys -if sys.version_info[0] < 3: +try: + import matlab.engine + from matlab.engine import MatlabExecutionError +except ImportError: + matlab = None + class MatlabExecutionError(Exception): + pass +from functools import partial +try: from StringIO import StringIO -else: +except ImportError: from io import StringIO -from metakernel import MetaKernel +import json +import os +import sys +try: + from tempfile import TemporaryDirectory +except ImportError: + from backports.tempfile import TemporaryDirectory + from IPython.display import Image +from metakernel import MetaKernel, ExceptionWrapper -# Import the correct engine -if 'OCTAVE_EXECUTABLE' in os.environ: - from pymatbridge import Octave - matlab_native = False -else: - try: - import matlab.engine - from matlab.engine import MatlabExecutionError - matlab_native = True - except ImportError: - try: - from pymatbridge import Matlab - matlab_native = False - except ImportError: - raise ImportError( - "Neither MATLAB native engine nor pymatbridge are available") +try: + from wurlitzer import pipes +except Exception: + pipes = None from . import __version__ +IS_CONNECT = "connect-to-existing-kernel" in os.environ.keys() +class _PseudoStream: -class MatlabEngine(object): + def __init__(self, writer): + self.write = writer - def __init__(self): - if 'OCTAVE_EXECUTABLE' in os.environ: - self._engine = Octave(os.environ['OCTAVE_EXECUTABLE']) - self._engine.start() - self.name = 'octave' - elif matlab_native: - self._engine = matlab.engine.start_matlab() - self.name = 'matlab' - else: - executable = os.environ.get('MATLAB_EXECUTABLE', 'matlab') - self._engine = Matlab(executable) - self._engine.start() - self.name = 'pymatbridge' - # add MATLAB-side helper functions to MATLAB's path - if self.name != 'octave': - kernel_path = os.path.dirname(os.path.realpath(__file__)) - toolbox_path = os.path.join(kernel_path, 'toolbox') - self.run_code("addpath('%s');" % toolbox_path) - - def run_code(self, code): - if matlab_native: - return self._run_native(code) - return self._engine.run_code(code) - - def stop(self): - if matlab_native: - self._engine.exit() - else: - self._engine.stop() - def _run_native(self, code): - resp = dict(success=True, content=dict()) - out = StringIO() - err = StringIO() - if sys.version_info[0] < 3: - code = str(code) - try: - self._engine.eval(code, nargout=0, stdout=out, stderr=err) - self._engine.eval(''' - figures = {}; - handles = get(0, 'children'); - for hi = 1:length(handles) - datadir = fullfile(tempdir(), 'MatlabData'); - if ~exist(datadir, 'dir'); mkdir(datadir); end - figures{hi} = [fullfile(datadir, ['MatlabFig', sprintf('%03d', hi)]), '.png']; - saveas(handles(hi), figures{hi}); - if (strcmp(get(handles(hi), 'visible'), 'off')); close(handles(hi)); end - end''', nargout=0, stdout=out, stderr=err) - figures = self._engine.workspace['figures'] - except (SyntaxError, MatlabExecutionError) as exc: - resp['content']['stdout'] = exc.args[0] - resp['success'] = False - else: - resp['content']['stdout'] = out.getvalue() - if figures: - resp['content']['figures'] = figures - return resp +def get_kernel_json(): + """Get the kernel json for the kernel. + """ + here = os.path.dirname(__file__) + kernel_name = 'matlab_connect' if IS_CONNECT else 'matlab' + with open(os.path.join(here, kernel_name ,'kernel.json')) as fid: + data = json.load(fid) + # data['argv'][0] = sys.executable + return data class MatlabKernel(MetaKernel): - implementation = 'Matlab Kernel' + app_name = 'matlab_kernel' + implementation = "Matlab Kernel" implementation_version = __version__, - language = 'matlab' + language = "matlab" language_version = __version__, banner = "Matlab Kernel" language_info = { - 'mimetype': 'text/x-octave', - 'codemirror_mode': 'octave', - 'name': 'matlab', - 'file_extension': '.m', - 'version': __version__, - 'help_links': MetaKernel.help_links, - } - kernel_json = { - "argv": [ - sys.executable, "-m", "matlab_kernel", "-f", "{connection_file}"], - "display_name": "Matlab", - "language": "matlab", "mimetype": "text/x-octave", + "codemirror_mode": "octave", "name": "matlab", + "file_extension": ".m", + "version": __version__, + "help_links": MetaKernel.help_links, } - - _first = True + kernel_json = get_kernel_json() def __init__(self, *args, **kwargs): super(MatlabKernel, self).__init__(*args, **kwargs) - self._matlab = MatlabEngine() + self.__matlab = None def get_usage(self): return "This is the Matlab kernel." + @property + def _matlab(self): + if self.__matlab: + return self.__matlab + + if matlab is None: + raise ImportError(""" + Matlab engine not installed: + See https://www.mathworks.com/help/matlab/matlab-engine-for-python.htm + """) + if IS_CONNECT: + try: + self.__matlab = matlab.engine.connect_matlab() + except matlab.engine.EngineError: + self.__matlab = matlab.engine.start_matlab() + else: + try: + self.__matlab = matlab.engine.start_matlab() + except matlab.engine.EngineError: + self.__matlab = matlab.engine.connect_matlab() + # detecting the correct kwargs for async running + # matlab 'async' param is deprecated since it became a keyword in python 3.7 + # instead, 'background' param is available and recommended since Matlab R2017b + self._async_kwargs = {'nargout': 0, 'async': True} + try: + self._matlab.eval('version', **self._async_kwargs) + except SyntaxError: + self._async_kwargs = {'nargout': 0, 'background': True} + self._validated_plot_settings = { + "backend": "inline", + "size": (560, 420), + "format": "png", + "resolution": 96, + } + self._validated_plot_settings["size"] = tuple( + self._matlab.get(0., "defaultfigureposition")[0][2:]) + self.handle_plot_settings() + return self.__matlab + def do_execute_direct(self, code): - if self._first: - self._first = False - self.handle_plot_settings() - - self.log.debug('execute: %s' % code) - resp = self._matlab.run_code(code.strip()) - self.log.debug('execute done') - if 'stdout' not in resp['content']: - raise ValueError(resp) - backend = self.plot_settings['backend'] - if 'figures' in resp['content'] and backend == 'inline': - for fname in resp['content']['figures']: - try: - im = Image(filename=fname) - self.Display(im) - except Exception as e: - self.Error(e) - if not resp['success']: - self.Error(resp['content']['stdout'].strip()) + if pipes: + retval = self._execute_async(code) else: - stdout = resp['content']['stdout'].strip() - if stdout: - self.Print(stdout) + retval = self._execute_sync(code) + + settings = self._validated_plot_settings + if settings["backend"] == "inline": + nfig = len(self._matlab.get(0., "children")) + if nfig: + with TemporaryDirectory() as tmpdir: + try: + self._matlab.eval( + "arrayfun(" + "@(h, i) print(h, sprintf('{}/%06i', i), '-d{}', '-r{}')," + "get(0, 'children'), ({}:-1:1)')".format( + '/'.join(tmpdir.split(os.sep)), + settings["format"], + settings["resolution"], + nfig), + nargout=0) + self._matlab.eval( + "arrayfun(@(h) close(h), get(0, 'children'))", + nargout=0) + for fname in sorted(os.listdir(tmpdir)): + self.Display(Image( + filename="{}/{}".format(tmpdir, fname))) + except Exception as exc: + self.Error(exc) + + return retval def get_kernel_help_on(self, info, level=0, none_on_fail=False): - obj = info.get('help_obj', '') - if not obj or len(obj.split()) > 1: - if none_on_fail: - return None - else: - return "" - code = 'help %s' % obj - resp = self._matlab.run_code(code.strip()) - return resp['content']['stdout'].strip() or None + name = info.get("help_obj", "") + out = StringIO() + self._matlab.help(name, nargout=0, stdout=out) + return out.getvalue() def get_completions(self, info): + """Get completions from kernel based on info dict. """ - Get completions from kernel based on info dict. - """ - if self._matlab.name != 'octave': - code = "do_matlab_complete('%s')" % info['obj'] - else: - code = 'completion_matches("%s")' % info['obj'] - resp = self._matlab.run_code(code.strip()) - return resp['content']['stdout'].strip().splitlines() or [] + + # Only MATLAB versions R2013a, R2014b, and R2015a were available for + # testing. This function is probably incompatible with some or many + # other releases, as the undocumented features it relies on are subject + # to change without notice. + + # grep'ing MATLAB R2014b for "tabcomplet" and dumping the symbols of + # the ELF files that match suggests that the internal tab completion + # is implemented in bin/glnxa64/libmwtabcompletion.so and called + # from /bin/glnxa64/libnativejmi.so, which contains the function + # mtFindAllTabCompletions. We can infer from MATLAB's undocumented + # naming conventions that this function can be accessed as a method of + # com.matlab.jmi.MatlabMCR objects. + + # Trial and error reveals likely function signatures for certain MATLAB + # versions. + # R2014b and R2015a: + # mtFindAllTabCompletions(String substring, int len, int offset) + # where `substring` is the string to be completed, `len` is the + # length of the string, and the first `offset` values returned by the + # engine are ignored. + # R2013a (not supported due to lack of Python engine): + # mtFindAllTabCompletions(String substring, int offset [optional]) + name = info["obj"] + compls = self._matlab.eval( + "cell(com.mathworks.jmi.MatlabMCR()." + "mtFindAllTabCompletions('{}', {}, 0))" + .format(name, len(name))) + + # For structs, we need to return `structname.fieldname` instead of just + # `fieldname`, which `mtFindAllTabCompletions` does. + # For tables also. + + if "." in name: + prefix, _ = name.rsplit(".", 1) + if self._matlab.eval("isstruct({})".format(prefix)) | self._matlab.eval("istable({})".format(prefix)): + compls = ["{}.{}".format(prefix, compl) for compl in compls] + + return compls + + def do_is_complete(self, code): + if self.parse_code(code)["magic"]: + return {"status": "complete"} + with TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "test_complete.m") + with open(path, mode='w') as f: + f.write(code) + self._matlab.eval( + "try, pcode {} -inplace; catch, end".format(tmpdir), + nargout=0) + if os.path.exists(os.path.join(tmpdir, "test_complete.p")): + return {"status": "complete"} + else: + return {"status": "incomplete"} def handle_plot_settings(self): - """Handle the current plot settings""" - settings = self.plot_settings - settings.setdefault('size', (560, 420)) - settings.setdefault('backend', 'inline') - - width, height = 560, 420 - if isinstance(settings['size'], tuple): - width, height = settings['size'] - elif settings['size']: + raw = self.plot_settings + settings = self._validated_plot_settings + + backends = {"inline": "off", "native": "on"} + backend = raw.get("backend") + if backend is not None: + if backend not in backends: + self.Error("Invalid backend, should be one of {}" + .format(sorted(list(backends)))) + else: + settings["backend"] = backend + + size = raw.get("size") + if size is not None: try: - width, height = settings['size'].split(',') - width, height = int(width), int(height) - settings['size'] = width, height - except Exception as e: - self.Error(e) - - if settings['backend'] == 'inline': - code = ["set(0, 'defaultfigurevisible', 'off')"] - else: - code = ["set(0, 'defaultfigurevisible', 'on')"] - paper_size = "set(0, 'defaultfigurepaperposition', [0 0 %s %s])" - figure_size = "set(0, 'defaultfigureposition', [0 0 %s %s])" - code += ["set(0, 'defaultfigurepaperunits', 'inches')", - "set(0, 'defaultfigureunits', 'inches')", - paper_size % (int(width) / 150., int(height) / 150.), - figure_size % (int(width) / 150., int(height) / 150.)] - if sys.platform == 'darwin' and self._matlab.name == 'octave': - code + ['setenv("GNUTERM", "X11")'] - if settings['backend'] != 'inline': - code += ["graphics_toolkit('%s');" % settings['backend']] - self._matlab.run_code(';'.join(code)) + width, height = size + except Exception as exc: + self.Error(exc) + else: + settings["size"] = size + if "width" in raw: + width, height = settings["size"] + raw.setdefault("width", width) + raw.setdefault("height", height) + settings["size"] = (raw["width"], raw["height"]) + + resolution = raw.get("resolution") + if resolution is not None: + settings["resolution"] = resolution + + backend = settings["backend"] + width, height = settings["size"] + resolution = settings["resolution"] + for k, v in { + "defaultfigurevisible": backends[backend], + "defaultfigurepaperpositionmode": "manual", + "defaultfigurepaperposition": + matlab.double([0, 0, width / resolution, height / resolution]), + "defaultfigurepaperunits": "inches"}.items(): + self._matlab.set(0., k, v, nargout=0) def repr(self, obj): return obj def restart_kernel(self): - """Restart the kernel""" - self._matlab.stop() + self._matlab.exit() + try: + self._matlab = matlab.engine.start_matlab() + except matlab.engine.EngineError: + # This isn't a true restart + self._matlab = None # disconnect from engine + self._matlab = matlab.engine.connect_matlab() # re-connect + self._matlab.clear('all') # clear all content + self.__matlab = None def do_shutdown(self, restart): - self._matlab.stop() + self._matlab.exit() + return super(MatlabKernel, self).do_shutdown(restart) + + def _execute_async(self, code): + try: + with pipes(stdout=_PseudoStream(partial(self.Print, end="")), + stderr=_PseudoStream(partial(self.Error, end=""))): + future = self._matlab.eval(code, **self._async_kwargs) + future.result() + except (SyntaxError, MatlabExecutionError, KeyboardInterrupt) as exc: + pass + #stdout = exc.args[0] + #return ExceptionWrapper("Error", -1, stdout) + + def _execute_sync(self, code): + out = StringIO() + err = StringIO() + if not isinstance(code, str): + code = code.encode('utf8') + try: + self._matlab.eval(code, nargout=0, stdout=out, stderr=err) + except (SyntaxError, MatlabExecutionError) as exc: + stdout = exc.args[0] + self.Error(stdout) + return ExceptionWrapper("Error", -1, stdout) + stdout = out.getvalue() + self.Print(stdout) + if __name__ == '__main__': try: diff --git a/matlab_kernel/kernel_template.json b/matlab_kernel/kernel_template.json new file mode 100644 index 0000000..adce8b8 --- /dev/null +++ b/matlab_kernel/kernel_template.json @@ -0,0 +1,8 @@ +{ + "argv": [ + "python", "-m", "matlab_kernel", "-f", "{connection_file}"], + "display_name": "Matlab", + "language": "matlab", + "mimetype": "text/x-octave", + "name": "matlab" +} diff --git a/matlab_kernel/toolbox/do_matlab_complete.m b/matlab_kernel/toolbox/do_matlab_complete.m deleted file mode 100644 index 96af815..0000000 --- a/matlab_kernel/toolbox/do_matlab_complete.m +++ /dev/null @@ -1,57 +0,0 @@ -function do_matlab_complete(substring) -%DO_MATLAB_COMPLETE list tab completion options for string -% do_matlab_complete(substring) prints out the tab completion options for the -% string `substring`, one per line. This required evaluating some undocumented -% internal matlab code in the "base" workspace. -% -% Only MATLAB versions R2013a, R2014b, and R2015a were available for testing. -% This function is probably incompatible with some or many other releases, as -% the undocumented features it relies on are subject to change without notice. - -% grep'ing MATLAB R2014b for "tabcomplet" and dumping the symbols of the ELF -% files that match suggests that the internal tab completion is implemented in -% bin/glnxa64/libmwtabcompletion.so and called from -% /bin/glnxa64/libnativejmi.so, which contains the function -% mtFindAllTabCompletions. We can infer from MATLAB's undocumented naming -% conventions that this function can be accessed as a method of -% com.matlab.jmi.MatlabMCR objects. - -% Trial and error reveals likely function signatures for certain MATLAB versions -% R2014b and R2015a: -% mtFindAllTabCompletions(String substring, int len, int offset) -% where `substring` is the string to be completed, `len` is the length of the -% string, and the first `offset` values returned by the engine are ignored. -% R2013a: -% mtFindAllTabCompletions(String substring, int offset [optional]) - -len = length(substring); -offset = 0; - -if verLessThan('MATLAB','8.4') - % verified for R2013a - args = sprintf('''%s'', %g', substring, offset); -else - % verified for R2014b, 2015a - args = sprintf('''%s'', %g, %g', substring, len, offset); -end - -% variables must be name-mangled to prevent collisions with user variables -% because this code will be executed in the user's actual workspace -get_completions = [ ... - 'matlabMCRinstance_avoid_name_collisions = com.mathworks.jmi.MatlabMCR;' ... - 'completions_output_avoid_name_collisions = ' ... - ' matlabMCRinstance_avoid_name_collisions.mtFindAllTabCompletions(' ... - args ... - ');' ... - 'prefix_avoid_name_collisions = get_completions_prefix(''' substring ''');' ... - 'for i=1:length(completions_output_avoid_name_collisions);' ... - ' fprintf(1, ''%s%s\n'', prefix_avoid_name_collisions, ' ... - ' char(completions_output_avoid_name_collisions(i)));' ... - 'end;' ... - 'clear(''matlabMCRinstance_avoid_name_collisions'', ' ... - ' ''completions_output_avoid_name_collisions'', ' ... - ' ''prefix_avoid_name_collisions'');' ]; - -try - evalin('base', get_completions); -end diff --git a/matlab_kernel/toolbox/get_completions_prefix.m b/matlab_kernel/toolbox/get_completions_prefix.m deleted file mode 100644 index 16f47ee..0000000 --- a/matlab_kernel/toolbox/get_completions_prefix.m +++ /dev/null @@ -1,24 +0,0 @@ -function prefix = get_completions_prefix(substr) -%get_completions_prefix shared prefix for completion results -% prefix = get_completions_prefix(substr) will return the prefix that needs -% to be prepended to the results of mtFindAllTabCompletions in order for -% Python's MetaKernel to use those results correctly. For now, this simply -% fixes a problem where MetaKernel framework expects -% do_matlab_complete('some_struct.long') to return -% `some_struct.long_fieldname`, but mtFindAllTabCompletions returns -% `long_fieldname` instead in this case. - prefix = ''; - period_ind = find(substr == '.'); - if isempty(period_ind) - return; - end - needs_prefix = false; - try - needs_prefix = evalin('base', ['isstruct(' substr(1:period_ind(1)-1) ')']); - catch - end - if needs_prefix - prefix = substr(1:period_ind(1)); - end -end - diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..097ca9e --- /dev/null +++ b/setup.cfg @@ -0,0 +1,10 @@ +[metadata] +license_file = LICENSE.txt + +[versioneer] +VCS = git +style = pep440 +versionfile_source = matlab_kernel/_version.py +versionfile_build = matlab_kernel/_version.py +tag_prefix = v +parentdir_prefix = matlab_kernel- diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..452985a --- /dev/null +++ b/setup.py @@ -0,0 +1,80 @@ +import glob +import json +import os +import sys +from setuptools import setup, find_packages + +with open('matlab_kernel/__init__.py', 'rb') as fid: + for line in fid: + line = line.decode('utf-8') + if line.startswith('__version__'): + version = line.strip().split()[-1][1:-1] + break + +DISTNAME = 'matlab_kernel' +PY_EXECUTABLE = 'python' + +# when building wheels, directly use 'python' in the kernelspec. +if any(a.startswith("bdist") for a in sys.argv): + PY_EXECUTABLE = 'python' + +# when directly installing, use sys.executable to get python full path. +if any(a.startswith("install") for a in sys.argv): + PY_EXECUTABLE = sys.executable + +# generating kernel.json for both kernels +os.makedirs(os.path.join(DISTNAME, 'matlab'), exist_ok=True) +with open(os.path.join(DISTNAME, 'kernel_template.json'), 'r') as fp: + matlab_json = json.load(fp) +matlab_json['argv'][0] = PY_EXECUTABLE +with open(os.path.join(DISTNAME, 'matlab','kernel.json'), 'w') as fp: + json.dump(matlab_json, fp) + +os.makedirs(os.path.join(DISTNAME, 'matlab_connect'), exist_ok=True) +with open(os.path.join(DISTNAME, 'kernel_template.json'), 'r') as fp: + matlab_json = json.load(fp) +matlab_json['argv'][0] = PY_EXECUTABLE +matlab_json['display_name'] = 'Matlab (Connection)' +matlab_json['name'] = "matlab_connect" +matlab_json['env'] = {'connect-to-existing-kernel': '1'} +with open(os.path.join(DISTNAME, 'matlab_connect','kernel.json'), 'w') as fp: + json.dump(matlab_json, fp) + +PACKAGE_DATA = { + DISTNAME: ['*.m'] + glob.glob('%s/**/*.*' % DISTNAME) +} +DATA_FILES = [ + ('share/jupyter/kernels/matlab', [ + '%s/matlab/kernel.json' % DISTNAME + ] + glob.glob('%s/images/*.png' % DISTNAME) + ), + ('share/jupyter/kernels/matlab_connect', [ + '%s/matlab_connect/kernel.json' % DISTNAME + ] + glob.glob('%s/images/*.png' % DISTNAME) + ) +] + +if __name__ == "__main__": + setup(name="matlab_kernel", + author="Steven Silvester, Antony Lee", + version=version, + url="https://github.com/Calysto/matlab_kernel", + license="BSD", + long_description=open("README.rst").read(), + long_description_content_type='text/x-rst', + classifiers=["Framework :: IPython", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Topic :: System :: Shells"], + packages=find_packages(include=["matlab_kernel", "matlab_kernel.*"]), + package_data=PACKAGE_DATA, + include_package_data=True, + data_files=DATA_FILES, + requires=["metakernel (>0.23.0)", "jupyter_client (>=4.4.0)", + "ipython (>=4.0.0)"], + install_requires=["metakernel>=0.23.0", "jupyter_client >=4.4.0", + "ipython>=4.0.0", + "backports.tempfile;python_version<'3.0'", + 'wurlitzer>=1.0.2;platform_system!="Windows"'] + ) diff --git a/test_matlab_kernel.py b/test_matlab_kernel.py index 212f819..5100596 100644 --- a/test_matlab_kernel.py +++ b/test_matlab_kernel.py @@ -1,27 +1,19 @@ """Example use of jupyter_kernel_test, with tests for IPython.""" import unittest -import jupyter_kernel_test as jkt -import os +from jupyter_kernel_test import KernelTests -os.environ['USE_OCTAVE'] = 'True' - -class MatlabKernelTests(jkt.KernelTests): +class MatlabKernelTests(KernelTests): kernel_name = "matlab" - language_name = "matlab" - code_hello_world = "disp('hello, world')" - completion_samples = [ - { - 'text': 'one', - 'matches': {'ones', 'onenormest'}, - }, + {'text': 'one', + 'matches': {'ones'}}, ] - code_page_something = "ones?" + if __name__ == '__main__': unittest.main()