Skip to content

Commit

Permalink
Merge pull request #45 from ccordoba12/ipdb-mpl-support
Browse files Browse the repository at this point in the history
PR: Add %matplotlib magic to the IPdb kernel
  • Loading branch information
ccordoba12 authored Sep 22, 2018
2 parents 952ece9 + 4f5c9b0 commit aab413a
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 28 deletions.
30 changes: 16 additions & 14 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
version: 2

jobs:
python2.7:
main: &main
machine: true
environment:
- PYTHON_VERSION: "2.7"
steps:
- checkout
- run:
command: docker pull continuumio/miniconda3:latest
command: docker pull dorowu/ubuntu-desktop-lxde-vnc:trusty
- run:
name: Install system packages
command: |
sudo apt-get -qq update
sudo apt-get install libegl1-mesa
- run:
command: ./.circleci/install.sh
- run:
command: ./.circleci/run_tests.sh

jobs:
python2.7:
python2.7:
<<: *main
environment:
- PYTHON_VERSION: "2.7"

python3.6:
machine: true
<<: *main
environment:
- PYTHON_VERSION: "3.6"
steps:
- checkout
- run:
command: docker pull continuumio/miniconda3:latest
- run:
command: ./.circleci/install.sh
- run:
command: ./.circleci/run_tests.sh

workflows:
version: 2
Expand Down
11 changes: 9 additions & 2 deletions .circleci/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

export TRAVIS_OS_NAME="linux"
export CONDA_DEPENDENCIES_FLAGS="--quiet"
export CONDA_DEPENDENCIES="ipykernel cloudpickle nomkl numpy pandas scipy pytest pytest-cov metakernel"
export PIP_DEPENDENCIES="codecov"
export CONDA_DEPENDENCIES="ipykernel cloudpickle nomkl numpy pandas scipy \
pexpect matplotlib qtconsole pytest pytest-cov"
export PIP_DEPENDENCIES_FLAGS="-q"
export PIP_DEPENDENCIES="codecov pytest-qt"

echo -e "PYTHON = $PYTHON_VERSION \n============"
git clone git://github.com/astropy/ci-helpers.git > /dev/null
source ci-helpers/travis/setup_conda_$TRAVIS_OS_NAME.sh
source $HOME/miniconda/etc/profile.d/conda.sh
conda activate test

# Install metakernel from master to verify that it doesn't break us
pip install -q --no-deps git+https://github.com/Calysto/metakernel.git
11 changes: 8 additions & 3 deletions .circleci/run_tests.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
#!/bin/bash

export PATH="$HOME/miniconda/bin:$PATH"
source activate test
source $HOME/miniconda/etc/profile.d/conda.sh
conda activate test

pytest -x -vv --cov=spyder_kernels --cov-report=term-missing spyder_kernels
# Install ipdb_kernel spec
pip install -e .
python spyder_kernels/ipdb install --user

# Run tests
pytest -x -vv --cov=spyder_kernels spyder_kernels

if [ $? -ne 0 ]; then
exit 1
Expand Down
55 changes: 55 additions & 0 deletions spyder_kernels/ipdb/backend_inline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2018- Spyder Kernels Contributors
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
# -----------------------------------------------------------------------------

"""
Functions for our inline backend.
This is a simplified version of some functions present in
ipykernel/pylab/backend_inline.py
"""

from ipykernel.pylab.backend_inline import _fetch_figure_metadata
from IPython.display import Image
from metakernel.display import display

from spyder_kernels.py3compat import io, PY2


def get_image(figure):
"""
Get image display object from a Matplotlib figure.
The idea to get png/svg from a figure was taken from
https://stackoverflow.com/a/12145161/438386
"""
# Print figure to a bytes stream
if PY2:
data = io.StringIO()
else:
data = io.BytesIO()
figure.canvas.print_figure(data, bbox_inches='tight')

# Get figure metadata
metadata = _fetch_figure_metadata(figure)

img = Image(data=data.getvalue(), metadata=metadata)
return img


def show():
"""
Show all figures as PNG payloads sent to the Jupyter clients.
"""
import matplotlib
from matplotlib._pylab_helpers import Gcf

try:
for figure_manager in Gcf.get_all_fig_managers():
display(get_image(figure_manager.canvas.figure))
finally:
if Gcf.get_all_fig_managers():
matplotlib.pyplot.close('all')
31 changes: 27 additions & 4 deletions spyder_kernels/ipdb/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
import functools
import sys

from ipykernel.eventloops import enable_gui
from IPython.core.completer import IPCompleter
from IPython.core.inputsplitter import IPythonInputSplitter
from IPython.core.interactiveshell import InteractiveShell
from IPython.core.debugger import BdbQuit_excepthook
from metakernel import MetaKernel

from spyder_kernels._version import __version__
from spyder_kernels.ipdb import backend_inline
from spyder_kernels.kernelmixin import BaseKernelMixIn
from spyder_kernels.ipdb.spyderpdb import SpyderPdb
from spyder_kernels.utils.module_completion import module_completion
Expand Down Expand Up @@ -96,15 +99,22 @@ def __init__(self, *args, **kwargs):
self.debugger.reset()
self.debugger.setup(sys._getframe().f_back, None)

# Completer
self.completer = IPCompleter(
shell=DummyShell(),
namespace=self._get_current_namespace()
)
self.completer.limit_to__all__ = False

# To detect if a line is complete
self.input_transformer_manager = IPythonInputSplitter(
line_input_checker=False)

# For the %matplotlib magic
self.ipyshell = InteractiveShell()
self.ipyshell.enable_gui = enable_gui
self.mpl_gui = None

self._remove_unneeded_magics()

# --- MetaKernel API
Expand All @@ -119,6 +129,8 @@ def do_execute_direct(self, code):
if stop:
self.debugger.postloop()

self._show_inline_figures()

def do_is_complete(self, code):
"""
Given code as string, returns dictionary with 'status' representing
Expand Down Expand Up @@ -176,16 +188,22 @@ def _remove_unneeded_magics(self):
"""Remove unnecessary magics from MetaKernel."""
line_magics = ['activity', 'conversation', 'dot', 'get', 'include',
'install', 'install_magic', 'jigsaw', 'kernel', 'kx',
'macro', 'parallel', 'pmap', 'px', 'run', 'scheme',
'set']
'macro', 'parallel', 'plot', 'pmap', 'px', 'run',
'scheme', 'set']
cell_magics = ['activity', 'brain', 'conversation', 'debug', 'dot',
'macro', 'processing', 'px', 'scheme', 'tutor']

for lm in line_magics:
self.line_magics.pop(lm)
try:
self.line_magics.pop(lm)
except KeyError:
pass

for cm in cell_magics:
self.cell_magics.pop(cm)
try:
self.cell_magics.pop(cm)
except:
pass

def _get_current_namespace(self, with_magics=False):
"""Get current namespace."""
Expand Down Expand Up @@ -238,3 +256,8 @@ def _phony_stdout(self, text):
'stream',
{'name': 'stdout',
'text': text})

def _show_inline_figures(self):
"""Show Matplotlib inline figures."""
if self.mpl_gui == 'inline':
backend_inline.show()
26 changes: 26 additions & 0 deletions spyder_kernels/ipdb/magics/matplotlib_magic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2018- Spyder Kernels Contributors
#
# Licensed under the terms of the MIT License
# (see spyder_kernels/__init__.py for details)
# -----------------------------------------------------------------------------

from metakernel import Magic


class MatplotlibMagic(Magic):

def line_matplotlib(self, gui):
"""
Matplotlib magic
You can set all backends you can with the IPython %matplotlib
magic.
"""
gui, backend = self.kernel.ipyshell.enable_matplotlib(gui=gui)
self.kernel.mpl_gui = gui


def register_magics(kernel):
kernel.register_magics(MatplotlibMagic)
14 changes: 10 additions & 4 deletions spyder_kernels/ipdb/spyderpdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ def notify_spyder(self, frame):
if not frame:
return

kernel = get_ipython().kernel
try:
kernel = get_ipython().kernel
except AttributeError:
return

# Get filename and line number of the current frame
fname = self.canonic(frame.f_code.co_filename)
Expand All @@ -87,7 +90,7 @@ def notify_spyder(self, frame):

# Set step of the current frame (if any)
step = {}
try:
try:
# Needed since basestring was removed in python 3
basestring
except NameError:
Expand Down Expand Up @@ -153,8 +156,11 @@ def _cmdloop(self):
@monkeypatch_method(pdb.Pdb, 'Pdb')
def reset(self):
self._old_Pdb_reset()
kernel = get_ipython().kernel
kernel._register_pdb_session(self)
try:
kernel = get_ipython().kernel
kernel._register_pdb_session(self)
except AttributeError:
pass


#XXX: notify spyder on any pdb command (is that good or too lazy? i.e. is more
Expand Down
3 changes: 2 additions & 1 deletion spyder_kernels/ipdb/tests/test_ipdb_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""
Tests for spyder_kernels.ipdb.kernel.py
"""

# Standart library imports
import os
import os.path as osp
Expand Down Expand Up @@ -62,7 +63,7 @@ def test_available_magics(ipdb_kernel):
'jump', 'l', 'latex', 'list',
'll', 'load', 'long_list', 'ls', 'lsmagic', 'magic',
'matplotlib', 'n', 'next', 'p', 'pdef', 'pdoc',
'pfile', 'pinfo', 'pinfo2', 'plot', 'pp', 'psource',
'pfile', 'pinfo', 'pinfo2', 'pp', 'psource',
'python', 'q', 'quit', 'r', 'reload_magics', 'restart',
'return', 'retval', 'rv', 's',
'shell', 'source', 'step', 'tbreak', 'u', 'unalias',
Expand Down
81 changes: 81 additions & 0 deletions spyder_kernels/ipdb/tests/test_matplotlib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (c) 2018- Spyder Kernels Contributors
#
# Licensed under the terms of the MIT License
# (see spyder_kernels/__init__.py for details)
# -----------------------------------------------------------------------------

from qtconsole.qtconsoleapp import JupyterQtConsoleApp
import pytest


SHELL_TIMEOUT = 20000


@pytest.fixture
def qtconsole(qtbot):
"""Qtconsole fixture."""
# Create a console with the ipdb kernel
console = JupyterQtConsoleApp()
console.initialize(argv=['--kernel', 'ipdb_kernel'])

qtbot.addWidget(console.window)
console.window.confirm_exit = False
console.window.show()
return console


def test_matplotlib_inline(qtconsole, qtbot):
"""Test that %matplotlib inline is working."""
window = qtconsole.window
shell = window.active_frontend

# Wait until the console is fully up
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)

# Set inline backend
with qtbot.waitSignal(shell.executed):
shell.execute("%matplotlib inline")

# Make a plot
with qtbot.waitSignal(shell.executed):
shell.execute("import matplotlib.pyplot as plt; plt.plot(range(10))")

# Assert that there's a plot in the console
assert shell._control.toHtml().count('img src') == 1


def test_matplotlib_qt(qtconsole, qtbot):
"""Test that %matplotlib qt is working."""
window = qtconsole.window
shell = window.active_frontend

# Wait until the console is fully up
qtbot.waitUntil(lambda: shell._prompt_html is not None,
timeout=SHELL_TIMEOUT)

# Set qt backend
with qtbot.waitSignal(shell.executed):
shell.execute("%matplotlib qt")

# Make a plot
with qtbot.waitSignal(shell.executed):
shell.execute("import matplotlib.pyplot as plt; plt.plot(range(10))")

# Assert we have three prompts in the console, meaning that the
# previous plot command was non-blocking
assert '3' in shell._prompt_html

# Running QApplication.instance() should return a QApplication
# object because "%matplotlib qt" creates one
with qtbot.waitSignal(shell.executed):
shell.execute("from PyQt5.QtWidgets import QApplication; QApplication.instance()")

# Assert the previous command returns the object
assert 'PyQt5.QtWidgets.QApplication object' in shell._control.toPlainText()


if __name__ == "__main__":
pytest.main()

0 comments on commit aab413a

Please sign in to comment.