├── .gitignore ├── requirements.txt ├── CHANGES.rst ├── .travis.yml ├── setup.py ├── pdb_ipython_nose.py ├── Example Usage.ipynb ├── README.rst ├── nose experiment.ipynb ├── test_ipython_nose.py └── ipython_nose.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipython>=4.0 2 | nose>=1.2.1,<2 3 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | ===== 3 | 4 | - added support for newer IPython versions 5 | - dropped support for IPython < 4.0 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: pip install -r requirements.txt --use-mirrors 6 | script: nosetests test_ipython_nose.py 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | 4 | with open('README.rst') as f: 5 | long_description = f.read() 6 | 7 | 8 | setup( 9 | name='ipython_nose', 10 | version='0.4.1', 11 | author='Taavi Burns , Greg Ward ', 12 | py_modules=['ipython_nose'], 13 | url='https://github.com/taavi/ipython_nose', 14 | license='README.rst', 15 | description='IPython extension to run nosetests against the current kernel.', 16 | long_description=long_description, 17 | ) 18 | -------------------------------------------------------------------------------- /pdb_ipython_nose.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | import ipython_nose 4 | 5 | test_module = types.ModuleType('test_module') 6 | from nose.plugins.skip import SkipTest 7 | 8 | def test_foo(): 9 | assert True 10 | 11 | def test_bar(): 12 | assert False 13 | 14 | def test_baz(): 15 | raise Exception() 16 | 17 | def test_quux(): 18 | raise SkipTest() 19 | 20 | test_module.test_foo = test_foo 21 | test_module.test_bar = test_bar 22 | test_module.test_baz = test_baz 23 | test_module.test_quux = test_quux 24 | 25 | plugin = ipython_nose.nose('', test_module) 26 | print plugin._repr_html_() 27 | -------------------------------------------------------------------------------- /Example Usage.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "Example Usage" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "code", 12 | "collapsed": false, 13 | "input": [ 14 | "%load_ext ipython_nose\n", 15 | "# %reload_ext ipython_nose" 16 | ], 17 | "language": "python", 18 | "metadata": {}, 19 | "outputs": [] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "collapsed": false, 24 | "input": [ 25 | "from nose import SkipTest\n", 26 | "\n", 27 | "def test_foo():\n", 28 | " assert True\n", 29 | " \n", 30 | "def test_bar():\n", 31 | " assert False\n", 32 | " \n", 33 | "def test_baz():\n", 34 | " raise SkipTest(\"Skipped\")\n", 35 | " \n", 36 | "import random\n", 37 | "import time\n", 38 | "def test_generate():\n", 39 | " for _ in range(random.randint(0, 10)):\n", 40 | " time.sleep(0.25)\n", 41 | " yield lambda x: None, _\n", 42 | " def fail(x):\n", 43 | " time.sleep(0.25)\n", 44 | " raise AssertionError(\"Failed\")\n", 45 | " for _ in range(random.randint(0, 10)):\n", 46 | " yield fail, _\n", 47 | " def skip(x):\n", 48 | " time.sleep(0.25)\n", 49 | " raise SkipTest(\"Skipped\")\n", 50 | " for _ in range(random.randint(0, 10)):\n", 51 | " yield skip, _" 52 | ], 53 | "language": "python", 54 | "metadata": {}, 55 | "outputs": [] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "collapsed": false, 60 | "input": [ 61 | "%nose" 62 | ], 63 | "language": "python", 64 | "metadata": {}, 65 | "outputs": [] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "collapsed": false, 70 | "input": [], 71 | "language": "python", 72 | "metadata": {}, 73 | "outputs": [] 74 | } 75 | ], 76 | "metadata": {} 77 | } 78 | ] 79 | } -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ipython_nose 2 | ------------ 3 | 4 | This little IPython extension gives you the ability to discover and 5 | run tests using Nose in an IPython Notebook. 6 | 7 | 8 | Installation 9 | ------------ 10 | 11 | * Make sure your IPython Notebook server can import ``ipython_nose.py`` (e.g. 12 | copy it to a directory in your ``PYTHONPATH``, or modify ``PYTHONPATH`` 13 | before starting IPython Notebook). It's also probably sufficient to have 14 | ``ipython_nose.py`` in the directory from which you run the notebook, e.g.:: 15 | 16 | $ ls 17 | ipython_nose.py 18 | $ ipython notebook 19 | 20 | * You can also install it in a virtualenv in developent mode:: 21 | 22 | $ cd ipython-nose 23 | $ pip install -e . 24 | 25 | 26 | Usage 27 | ----- 28 | 29 | * Add a cell containing:: 30 | 31 | %load_ext ipython_nose 32 | 33 | somewhere in your notebook. 34 | 35 | * Write tests that conform to Nose conventions, e.g.:: 36 | 37 | def test_arithmetic(): 38 | assert 1+1 == 2 39 | 40 | * Add a cell consisting of:: 41 | 42 | %nose 43 | 44 | to your notebook and run that cell. That will discover your 45 | ``test_*`` functions, run them, and report how many passed and 46 | how many failed, with stack traces for each failure. 47 | 48 | * Pass standard nose arguments to the magic:: 49 | 50 | %nose -v -x 51 | 52 | ``-v`` is handled specially, but other arguments are passed to nosetests as 53 | if they were passed at the command-line. 54 | 55 | * Only run test-like things in the current cell using the ``%%nose`` cell magic, 56 | e.g.:: 57 | 58 | %%nose 59 | 60 | def test_just_this(): 61 | assert True 62 | 63 | 64 | Caveats 65 | ------- 66 | 67 | * Renaming tests leaves behind the old name: you might only see N 68 | test methods in your notebook, but Nose will discover and run N+1 69 | tests. Not sure how to fix this one. 70 | 71 | * Links between the stack traces and the code are only cell deep. For example, 72 | in a stack trace that looks like:: 73 | 74 | Traceback (most recent call last): 75 | File "/usr/lib/python2.7/dist-packages/nose/case.py", line 197, in runTest 76 | self.test(*self.arg) 77 | File "", line 2, in test_myfunc 78 | assert myfunc() == 42 79 | AssertionError 80 | 81 | the frame name ``ipython-input-10-a3ae96abafeb`` is a link to cell 10, but 82 | not specifically to line 2. 83 | 84 | 85 | Authors 86 | ------- 87 | 88 | * Taavi Burns 89 | * Greg Ward 90 | 91 | Thanks to Fernando Perez and Greg Wilson for tips, ideas, etc. 92 | 93 | Thanks to Catherine Devlin for publishing `ipython_doctester 94 | `_ so we could peek 95 | at its guts. 96 | 97 | 98 | Get the code 99 | ------------ 100 | 101 | :: 102 | 103 | git clone https://github.com/taavi/ipython_nose.git 104 | 105 | 106 | Copyright 107 | --------- 108 | 109 | Copyright (c) 2012, Taavi Burns, Greg Ward. 110 | 111 | All rights reserved. 112 | 113 | Redistribution and use in source and binary forms, with or without 114 | modification, are permitted provided that the following conditions are met: 115 | 116 | Redistributions of source code must retain the above copyright notice, this 117 | list of conditions and the following disclaimer. 118 | 119 | Redistributions in binary form must reproduce the above copyright notice, this 120 | list of conditions and the following disclaimer in the documentation and/or 121 | other materials provided with the distribution. 122 | 123 | Neither the name of the developers nor the names of contributors may 124 | be used to endorse or promote products derived from this software 125 | without specific prior written permission. 126 | 127 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 128 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 129 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 130 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 131 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 132 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 133 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 134 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 135 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 136 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 137 | -------------------------------------------------------------------------------- /nose experiment.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "name": "nose experiment" 4 | }, 5 | "nbformat": 3, 6 | "nbformat_minor": 0, 7 | "worksheets": [ 8 | { 9 | "cells": [ 10 | { 11 | "cell_type": "code", 12 | "collapsed": false, 13 | "input": [ 14 | "import sys, os\n", 15 | "import logging\n", 16 | "import unittest\n", 17 | "\n", 18 | "log = logging.getLogger()\n", 19 | "\n", 20 | "from nose import core, loader\n", 21 | "\n", 22 | "#logging.basicConfig(level=logging.DEBUG)\n", 23 | "\n", 24 | "from types import ModuleType" 25 | ], 26 | "language": "python", 27 | "metadata": {}, 28 | "outputs": [], 29 | "prompt_number": 1 30 | }, 31 | { 32 | "cell_type": "code", 33 | "collapsed": false, 34 | "input": [ 35 | "def adder(a, b):\n", 36 | " return a + b\n", 37 | "\n", 38 | "def test_nothing():\n", 39 | " assert 1+1 == 2\n", 40 | "\n", 41 | "def test_adder():\n", 42 | " assert adder(3, 4) == 7\n", 43 | " assert adder(-3, -5) == -8\n", 44 | " \n", 45 | "import random\n", 46 | "def test_random_generated_tests():\n", 47 | " for _ in range(random.randint(1, 100)):\n", 48 | " yield lambda: None\n", 49 | "\n", 50 | "flake = False\n", 51 | "def test_flake():\n", 52 | " global flake\n", 53 | " flake = not flake\n", 54 | " assert flake" 55 | ], 56 | "language": "python", 57 | "metadata": {}, 58 | "outputs": [], 59 | "prompt_number": 2 60 | }, 61 | { 62 | "cell_type": "code", 63 | "collapsed": false, 64 | "input": [ 65 | "class MyProgram(core.TestProgram):\n", 66 | " # XXX yuck: copy superclass runTests() so we can instantiate our own runner class;\n", 67 | " # can't do it early because we don't have access to nose's config object.\n", 68 | " def runTests(self):\n", 69 | " self.testRunner = MyRunner(self.config)\n", 70 | " # the rest is mostly duplicate code ;-(\n", 71 | " plug_runner = self.config.plugins.prepareTestRunner(self.testRunner)\n", 72 | " if plug_runner is not None:\n", 73 | " self.testRunner = plug_runner\n", 74 | " self.result = self.testRunner.run(self.test)\n", 75 | " self.success = self.result.wasSuccessful()\n", 76 | " return self.success\n", 77 | "\n", 78 | "class MyResult(unittest.TestResult):\n", 79 | " def make_bar(self, tests, failing):\n", 80 | " return '''
\n", 81 | "
 
\n", 82 | "
 
\n", 83 | "
''' % (\n", 84 | " failing * 10, (tests - failing) * 10)\n", 85 | "\n", 86 | " def make_table_of_tests(self, tests):\n", 87 | " table = ''\n", 88 | " for test in tests:\n", 89 | " table += '' % (\n", 90 | " str(test[0]), test[1])\n", 91 | " table += '
%s
'\n", 92 | " return table\n", 93 | " \n", 94 | " def _repr_html_(self):\n", 95 | " if self.errors or self.failures:\n", 96 | " not_successes = len(self.errors) + len(self.failures)\n", 97 | " return self.make_bar(self.testsRun, not_successes) + \\\n", 98 | " '''\n", 99 | "

Errors

%s\n", 100 | "

Failures

%s''' % (\n", 101 | " self.make_table_of_tests(self.errors),\n", 102 | " self.make_table_of_tests(self.failures))\n", 103 | " else:\n", 104 | " return self.make_bar(self.testsRun, 0) + '
%d/%d tests passed
' % (\n", 105 | " self.testsRun, self.testsRun)\n", 106 | "\n", 107 | "class MyRunner(object):\n", 108 | " def __init__(self, config):\n", 109 | " self.config = config\n", 110 | "\n", 111 | " def run(self, test):\n", 112 | " result = MyResult()\n", 113 | " if hasattr(result, 'startTestRun'): # python 2.7\n", 114 | " result.startTestRun()\n", 115 | " test(result)\n", 116 | " if hasattr(result, 'stopTestRun'):\n", 117 | " result.stopTestRun()\n", 118 | " self.config.plugins.finalize(result)\n", 119 | " self.result = result\n", 120 | " return result" 121 | ], 122 | "language": "python", 123 | "metadata": {}, 124 | "outputs": [], 125 | "prompt_number": 3 126 | }, 127 | { 128 | "cell_type": "code", 129 | "collapsed": false, 130 | "input": [ 131 | "test_module = ModuleType('test_module')\n", 132 | "test_module.__dict__.update(get_ipython().user_ns)\n", 133 | "\n", 134 | "ldr = loader.TestLoader()\n", 135 | "tests = ldr.loadTestsFromModule(test_module)\n", 136 | "#print(\"discovered: %r\" % list(tests._tests))\n", 137 | "\n", 138 | "tprog = MyProgram(argv=['dummy'], exit=False, suite=tests)\n", 139 | "tprog.result" 140 | ], 141 | "language": "python", 142 | "metadata": {}, 143 | "outputs": [ 144 | { 145 | "html": [ 146 | "
\n", 147 | "
 
\n", 148 | "
 
\n", 149 | "
62/62 tests passed
" 150 | ], 151 | "output_type": "pyout", 152 | "prompt_number": 4, 153 | "text": [ 154 | "<__main__.MyResult run=62 errors=0 failures=0>" 155 | ] 156 | } 157 | ], 158 | "prompt_number": 4 159 | } 160 | ], 161 | "metadata": {} 162 | } 163 | ] 164 | } -------------------------------------------------------------------------------- /test_ipython_nose.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from nose.tools import eq_ 4 | 5 | import ipython_nose 6 | 7 | 8 | def assert_in(subject, container): 9 | if subject not in container: 10 | raise AssertionError("'%s' not in '%s'" % (subject, container)) 11 | 12 | 13 | def assert_not_in(subject, container): 14 | if subject in container: 15 | raise AssertionError("'%s' contains '%s'" % (container, subject)) 16 | 17 | 18 | def get_raised_exception_tuple_with_message(message): 19 | try: 20 | raise Exception(message) 21 | except Exception as e: 22 | return sys.exc_info() 23 | 24 | 25 | class TestTemplate(object): 26 | def test_str(self): 27 | template = ipython_nose.Template('{var}') 28 | eq_('val') 147 | tracebacks = self.plugin._tracebacks( 148 | [ 149 | (FakeTest(), exception_tuple), 150 | ], 151 | self.plugin._tracebacks_template_html 152 | ) 153 | assert_in('<', tracebacks) 154 | 155 | def test_tracebacks_in_html_escapes_traceback(self): 156 | exception_tuple = get_raised_exception_tuple_with_message('>') 157 | tracebacks = self.plugin._tracebacks( 158 | [ 159 | (FakeTest(), exception_tuple), 160 | ], 161 | self.plugin._tracebacks_template_html 162 | 163 | ) 164 | assert_in('>', tracebacks) 165 | 166 | def test_tracebacks_in_text_does_not_escape_test_name(self): 167 | exception_tuple = get_raised_exception_tuple_with_message('>') 168 | tracebacks = self.plugin._tracebacks( 169 | [ 170 | (FakeTest(), exception_tuple), 171 | ], 172 | self.plugin._tracebacks_template_text 173 | ) 174 | assert_in('>', tracebacks) 175 | 176 | def test_tracebacks_in_text_does_not_escape_traceback(self): 177 | exception_tuple = get_raised_exception_tuple_with_message('>') 178 | tracebacks = self.plugin._tracebacks( 179 | [ 180 | (FakeTest(), exception_tuple), 181 | ], 182 | self.plugin._tracebacks_template_text 183 | 184 | ) 185 | assert_in('>', tracebacks) 186 | 187 | def test_linkify_html_traceback(self): 188 | frame_number = '1' 189 | frame_name = 'ipython-input-' + frame_number + '-0123456789ab' 190 | linkified = self.plugin.linkify_html_traceback( 191 | '<' + frame_name + '>') 192 | expected_link = ( 193 | '<' + 196 | frame_name + 197 | '') 198 | assert expected_link in linkified, "\n%s\nnot in\n%s" % ( 199 | expected_link, linkified) 200 | expected_selector = ( 201 | '$("div.prompt.input_prompt:contains([' + 202 | frame_number + 203 | '])")') 204 | assert expected_selector in linkified, "\n%s\nnot in\n%s" % ( 205 | expected_selector, linkified) 206 | -------------------------------------------------------------------------------- /ipython_nose.py: -------------------------------------------------------------------------------- 1 | import cgi 2 | import os 3 | import traceback 4 | import re 5 | import shlex 6 | import string 7 | import types 8 | import uuid 9 | import json 10 | 11 | from nose import core as nose_core 12 | from nose import loader as nose_loader 13 | from nose.config import Config, all_config_files 14 | from nose.plugins.base import Plugin 15 | from nose.plugins.skip import SkipTest 16 | from nose.plugins.manager import DefaultPluginManager 17 | from nose.selector import defaultSelector 18 | from IPython.core import magic 19 | from IPython.display import display 20 | 21 | 22 | class Template(string.Formatter): 23 | def __init__(self, template): 24 | self._template = template 25 | 26 | def format(self, **context): 27 | return self.vformat(self._template, (), context) 28 | 29 | def convert_field(self, value, conversion): 30 | if conversion == 'e': 31 | return cgi.escape(value) 32 | else: 33 | return super(Template, self).convert_field(value, conversion) 34 | 35 | 36 | class DummyUnittestStream: 37 | def write(self, *arg): 38 | pass 39 | 40 | def writeln(self, *arg): 41 | pass 42 | 43 | def flush(self, *arg): 44 | pass 45 | 46 | 47 | class NotebookLiveOutput(object): 48 | def __init__(self): 49 | self.output_id = 'ipython_nose_%s' % uuid.uuid4().hex 50 | 51 | def finalize(self, pass_or_fail, n_tests, n_failures, n_errors, tests): 52 | # tell frontend whether tests passed or failed 53 | out = { 54 | "success": pass_or_fail, 55 | "summary": { 56 | "tests": n_tests, # the total number of tests, 57 | "failures": n_failures, # the number of tests that failed 58 | "errors": n_errors # The number of tests that errored out, which mean it couldn't run but dind' 59 | }, 60 | "tests": list(map(self._dump_test, tests)) 61 | } 62 | 63 | output = {'application/json': json.dumps(out)} 64 | display(output, raw = True) 65 | return output 66 | 67 | def write_chars(self, chars): 68 | pass 69 | 70 | def write_line(self, line): 71 | pass 72 | 73 | def _dump_test(self, test_tuple): 74 | test, exc, res = test_tuple 75 | return { "name": test.shortDescription() or str(test), 76 | "success": res == "success", 77 | "message": ''.join(traceback.format_exception(*exc)[-1:]) if res != "success" else ""} 78 | 79 | 80 | def html_escape(s): 81 | return cgi.escape(str(s)) 82 | 83 | 84 | class IPythonDisplay(Plugin): 85 | """Do something nice in IPython.""" 86 | 87 | name = 'ipython-html' 88 | enabled = True 89 | score = 2 90 | 91 | def __init__(self, verbose=False, expand_tracebacks=False): 92 | super(IPythonDisplay, self).__init__() 93 | self.verbose = verbose 94 | self.expand_tracebacks = expand_tracebacks 95 | self.html = [] 96 | self.num_tests = 0 97 | self.failures = [] 98 | self.tests = [] 99 | self.skipped = 0 100 | self.json_output = "" 101 | 102 | _summary_template_text = Template('''{text}\n''') 103 | 104 | def _summary(self, numtests, numfailed, numskipped, template): 105 | text = "%d/%d tests passed" % (numtests - numfailed, numtests) 106 | if numfailed > 0: 107 | text += "; %d failed" % numfailed 108 | if numskipped > 0: 109 | text += "; %d skipped" % numskipped 110 | 111 | failpercent = int(float(numfailed) / numtests * 100) 112 | if numfailed > 0 and failpercent < 5: 113 | # Ensure the red bar is visible 114 | failpercent = 5 115 | 116 | skippercent = int(float(numskipped) / numtests * 100) 117 | if numskipped > 0 and skippercent < 5: 118 | # Ditto for the yellow bar 119 | skippercent = 5 120 | 121 | passpercent = 100 - failpercent - skippercent 122 | 123 | return template.format( 124 | text=text, failpercent=failpercent, skippercent=skippercent, 125 | passpercent=passpercent) 126 | 127 | _tracebacks_template_text = Template( 128 | '''========\n{name}\n========\n{formatted_traceback}\n''') 129 | 130 | def _tracebacks(self, failures, template): 131 | output = [] 132 | for test, exc, res in failures: 133 | name = test.shortDescription() or str(test) 134 | formatted_traceback = ''.join(traceback.format_exception(*exc)) 135 | output.append(template.format( 136 | name=name, formatted_traceback=formatted_traceback, 137 | hide_traceback_style=('block' if self.expand_tracebacks 138 | else 'none') 139 | )) 140 | return ''.join(output) 141 | 142 | def _write_test_line(self, test, status): 143 | self.live_output.write_line( 144 | "{} ... {}".format(test.shortDescription() or str(test), status)) 145 | 146 | def addSuccess(self, test): 147 | if self.verbose: 148 | self._write_test_line(test, 'pass') 149 | else: 150 | self.live_output.write_chars('.') 151 | self.tests.append((test, None, 'success')) 152 | 153 | def addError(self, test, err): 154 | if issubclass(err[0], SkipTest): 155 | return self.addSkip(test) 156 | if self.verbose: 157 | self._write_test_line(test, 'error') 158 | else: 159 | self.live_output.write_chars('E') 160 | self.failures.append((test, err, 'error')) 161 | self.tests.append((test, err, 'error')) 162 | 163 | def addFailure(self, test, err): 164 | if self.verbose: 165 | self._write_test_line(test, 'fail') 166 | else: 167 | self.live_output.write_chars('F') 168 | self.failures.append((test, err, 'failure')) 169 | self.tests.append((test, err, 'failure')) 170 | 171 | # Deprecated in newer versions of nose; skipped tests are handled in 172 | # addError in newer versions 173 | def addSkip(self, test): 174 | if self.verbose: 175 | self.live_output.write_line(str(test) + " ... SKIP") 176 | else: 177 | self.live_output.write_chars('S') 178 | self.skipped += 1 179 | 180 | def begin(self): 181 | self.live_output = NotebookLiveOutput() 182 | 183 | def finalize(self, result): 184 | self.result = result 185 | self.json_output = self.live_output.finalize( 186 | result.wasSuccessful(), 187 | self.num_tests, self.n_failures, self.n_errors, 188 | self.tests) 189 | 190 | def setOutputStream(self, stream): 191 | # grab for own use 192 | self.stream = stream 193 | return DummyUnittestStream() 194 | 195 | def startContext(self, ctx): 196 | pass 197 | 198 | def stopContext(self, ctx): 199 | pass 200 | 201 | def startTest(self, test): 202 | self.num_tests += 1 203 | 204 | def stopTest(self, test): 205 | pass 206 | 207 | @property 208 | def n_failures(self): 209 | return len([entry for entry in self.failures if entry[2] == 'failure']) 210 | 211 | @property 212 | def n_errors(self): 213 | return len([entry for entry in self.failures if entry[2] == 'error']) 214 | 215 | def _repr_pretty_(self, p, cycle): 216 | if self.num_tests <= 0: 217 | p.text('No tests found.') 218 | return 219 | p.text(self._summarize()) 220 | p.text(self._summarize_tracebacks()) 221 | 222 | def _summarize(self): 223 | return self._summary(self.num_tests, len(self.failures), 224 | self.skipped, self._summary_template_text) 225 | 226 | def _summarize_tracebacks(self): 227 | return self._tracebacks(self.failures, self._tracebacks_template_text) 228 | 229 | 230 | def get_ipython_user_ns_as_a_module(): 231 | test_module = types.ModuleType('test_module') 232 | test_module.__dict__.update(get_ipython().user_ns) 233 | return test_module 234 | 235 | 236 | def makeNoseConfig(env): 237 | """Load a Config, pre-filled with user config files if any are 238 | found. 239 | """ 240 | cfg_files = all_config_files() 241 | manager = DefaultPluginManager() 242 | return Config(env=env, files=cfg_files, plugins=manager) 243 | 244 | 245 | class ExcludingTestSelector(defaultSelector): 246 | def __init__(self, config, excluded_objects): 247 | super(ExcludingTestSelector, self).__init__(config) 248 | self.excluded_objects = list(excluded_objects) 249 | 250 | def _in_excluded_objects(self, obj): 251 | for excluded_object in self.excluded_objects: 252 | try: 253 | if obj is excluded_object: 254 | return True 255 | except Exception: 256 | return False 257 | return False 258 | 259 | def wantClass(self, cls): 260 | if self._in_excluded_objects(cls): 261 | return False 262 | else: 263 | return super(ExcludingTestSelector, self).wantClass(cls) 264 | 265 | def wantFunction(self, function): 266 | if self._in_excluded_objects(function): 267 | return False 268 | else: 269 | return super(ExcludingTestSelector, self).wantFunction(function) 270 | 271 | def wantMethod(self, method): 272 | if self._in_excluded_objects(type(method.__self__)): 273 | return False 274 | else: 275 | return super(ExcludingTestSelector, self).wantMethod(method) 276 | 277 | 278 | def nose(line, cell=None, test_module=get_ipython_user_ns_as_a_module): 279 | if callable(test_module): 280 | test_module = test_module() 281 | config = makeNoseConfig(os.environ) 282 | if cell is None: 283 | # Called as the %nose line magic. 284 | # All objects in the notebook namespace should be considered for the 285 | # test suite. 286 | selector = None 287 | else: 288 | # Called as the %%nose cell magic. 289 | # Classes and functions defined outside the cell should be excluded from 290 | # the test run. 291 | selector = ExcludingTestSelector(config, test_module.__dict__.values()) 292 | # Evaluate the cell and add objects it defined into the test module. 293 | exec(cell, test_module.__dict__) 294 | loader = nose_loader.TestLoader(config=config, selector=selector) 295 | tests = loader.loadTestsFromModule(test_module) 296 | extra_args = shlex.split(str(line)) 297 | expand_tracebacks = '--expand-tracebacks' in extra_args 298 | if expand_tracebacks: 299 | extra_args.remove('--expand-tracebacks') 300 | argv = ['ipython-nose', '--with-ipython-html', '--no-skip'] + extra_args 301 | verbose = '-v' in extra_args 302 | plug = IPythonDisplay(verbose=verbose, expand_tracebacks=expand_tracebacks) 303 | 304 | nose_core.TestProgram( 305 | argv=argv, suite=tests, addplugins=[plug], exit=False, config=config) 306 | 307 | ## for debugging 308 | #get_ipython().user_ns['nose_obj'] = plug 309 | return plug 310 | 311 | 312 | def load_ipython_extension(ipython): 313 | magic.register_line_cell_magic(nose) 314 | --------------------------------------------------------------------------------