├── .gitignore ├── CHANGES.txt ├── LICENSE.txt ├── README.txt ├── ipython_doctester.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | .coverage 21 | .tox 22 | 23 | #Translations 24 | *.mo 25 | 26 | #Mr Developer 27 | .mr.developer.cfg 28 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1.0, Nov 3 2012 -- Initial release 2 | v0.2.0, Dec 16 2012 -- optional ``verbose``; report results to ipython-docent.appspot.com 3 | v0.2.1, Dec 16 2012 -- include requests module in requirements 4 | v0.2.2, Mar 14 2013 -- include ipython-docent.appspot.com integration 5 | v0.2.3, Sep 24 2013 -- support of IPython 1.x (zmq in the kernel) 6 | v0.3.0, Oct 12 2013 -- Inject doctests from .txt files 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012 Catherine Devlin (catherine.devlin@gmail.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | 11 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | ================= 2 | ipython_doctester 3 | ================= 4 | 5 | Lets you run the doctests of a single class or function at a time. Useful for 6 | tutorials based on the IPython Notebook, using doctests for student feeback. 7 | 8 | Install with ``pip install ipython_doctester``, or 9 | navigate to this directory and run:: 10 | 11 | python setup.py install 12 | 13 | Use 14 | === 15 | 16 | Run ``ipython notebook``, then start your notebook with this import:: 17 | 18 | In [1]: from ipython_doctester import test 19 | 20 | In each subsequent cell, set up objects with their doctests, and with absent 21 | (or flawed) function bodies, and decorate them with @test:: 22 | 23 | In [2]: @test 24 | def square(x): 25 | ''' 26 | >>> f(2) 27 | 4 28 | ''' 29 | 30 | Tests will run on each cell as it is executed. 31 | 32 | If you want to track students' progress through a notebook in a 33 | classroom setting, you can; see 34 | http://ipython-docent.appspot.com/ 35 | for instructions. 36 | 37 | If no doctests are found in the function's docstring, the program will look 38 | for a file ``./docstrings/.txt``, append that to the function's 39 | docstring, and check for doctests again. This can be used to keep the presence 40 | of the docstrings from confusing students. 41 | 42 | Development 43 | =========== 44 | 45 | https://github.com/catherinedevlin/ipython_doctester 46 | 47 | Thanks to 48 | ========= 49 | 50 | Brian Granger for technical advice 51 | -------------------------------------------------------------------------------- /ipython_doctester.py: -------------------------------------------------------------------------------- 1 | 2 | """Run doctests on a single class or function, and report for IPython Notebook. 3 | 4 | Decorate each function or class to be tested with ``ipython_doctester.test``. 5 | 6 | If you want to turn off automatic testing but don't want to take the @test 7 | decorators off, set ipython_doctester.run_tests = False. 8 | 9 | Note: It's easy to cheat by simply deleting or changing the doctest. That's 10 | OK, cheating is learning, too. 11 | 12 | If you want to track students' progress through a notebook in a 13 | classroom setting, you can; see 14 | http://ipython-docent.appspot.com/ 15 | for instructions. 16 | 17 | Developed for the Dayton Python Workshop: 18 | https://openhatch.org/wiki/Dayton_Python_Workshop 19 | catherine.devlin@gmail.com 20 | 21 | """ 22 | 23 | import IPython 24 | import doctest 25 | import cgi 26 | import inspect 27 | import sys 28 | import os 29 | import os.path 30 | import requests 31 | 32 | 33 | from IPython.core.displaypub import publish_display_data 34 | 35 | try: 36 | from IPython.zmq import displayhook as zmq_displayhook 37 | except ImportError: 38 | # zmq in kernel in Ipython 1.x serie 39 | # http://ipython.org/ipython-doc/rel-1.0.0/whatsnew/version1.0.html 40 | from IPython.kernel.zmq import displayhook as zmq_displayhook 41 | 42 | 43 | __version__ = '0.3.0' 44 | finder = doctest.DocTestFinder() 45 | docent_url = 'http://ipython-docent.appspot.com' 46 | doctest_path = './doctests' 47 | 48 | """Set these per session, as desired.""" 49 | run_tests = True 50 | verbose = False # ``True`` causes the result table to print, 51 | # even for successes 52 | 53 | """Set these if desired to track student progress 54 | at http://ipython-docent.appspot.com/. 55 | See that page for more instructions.""" 56 | student_name = None 57 | workshop_name = None 58 | 59 | 60 | def running_from_notebook(): 61 | return isinstance(sys.displayhook, zmq_displayhook.ZMQShellDisplayHook) 62 | 63 | 64 | class Reporter(object): 65 | html = running_from_notebook() 66 | 67 | def __init__(self): 68 | self.failed = False 69 | self.examples = [] 70 | self.txt = '' 71 | 72 | example_template = ('
%s
' 73 | '
%s
' 74 | '
%s
') 75 | fail_template = """ 76 |

Oops! Not quite there yet...

77 | """ 78 | success_template = """ 79 |

Success!

80 | """ 81 | 82 | def trap_txt(self, txt): 83 | self.txt += txt 84 | 85 | def publish(self): 86 | # if self.html: 87 | # IPython.core.displaypub.publish_html(self._repr_html_()) 88 | # else: 89 | # IPython.core.displaypub.publish_pretty(self.txt) 90 | 91 | if self.html: 92 | publish_display_data("ipython_doctester", 93 | {'text/html': self._repr_html_()}) 94 | else: 95 | publish_display_data("ipython_doctester", 96 | {'text/plain': self.txt}) 97 | 98 | def _repr_html_(self): 99 | result = self.fail_template if self.failed else self.success_template 100 | if verbose or self.failed: 101 | examples = '\n '.join(self.example_template % 102 | (cgi.escape(e.source), 103 | cgi.escape(e.want), 104 | e.color, cgi.escape(e.got) 105 | )for e in self.examples) 106 | result += (""" 107 | 108 | """ 109 | + examples + """ 110 |
TriedExpectedGot
111 | """) 112 | return result 113 | 114 | 115 | reporter = Reporter() 116 | 117 | 118 | class Runner(doctest.DocTestRunner): 119 | 120 | def _or_nothing(self, x): 121 | if x in (None, ''): 122 | return 'Nothing' 123 | elif hasattr(x, 'strip') and x.strip() == '': 124 | return '' 125 | return x 126 | 127 | def report_failure(self, out, test, example, got): 128 | example.got = self._or_nothing(got) 129 | example.want = self._or_nothing(example.want) 130 | example.color = 'red' 131 | reporter.examples.append(example) 132 | reporter.failed = True 133 | return doctest.DocTestRunner.report_failure(self, out, test, example, 134 | got) 135 | 136 | def report_success(self, out, test, example, got): 137 | example.got = self._or_nothing(got) 138 | example.want = self._or_nothing(example.want) 139 | example.color = 'green' 140 | reporter.examples.append(example) 141 | return doctest.DocTestRunner.report_success(self, out, test, example, got) 142 | 143 | def report_unexpected_exception(self, out, test, example, exc_info): 144 | reporter.failed = True 145 | trim = len(reporter.txt) 146 | result = doctest.DocTestRunner.report_unexpected_exception( 147 | self, out, test, example, exc_info) 148 | example.got = reporter.txt[trim:].split('Exception raised:')[1] 149 | example.want = self._or_nothing(example.want) 150 | example.color = 'red' 151 | reporter.examples.append(example) 152 | return result 153 | 154 | 155 | runner = Runner() 156 | finder = doctest.DocTestFinder() 157 | 158 | 159 | class IPythonDoctesterException(Exception): 160 | 161 | def _repr_html_(self): 162 | return '
\n%s\n
' % self.txt 163 | 164 | 165 | class NoTestsException(IPythonDoctesterException): 166 | txt = """ 167 | OOPS! We expected to find a doctest - 168 | a string immediately after the function definition, looking something like 169 | def do_something(): 170 | ''' 171 | >>> do_something() 172 | 'did something' 173 | ''' 174 | ... but it wasn't there. Did you insert code between the function definition 175 | and the doctest? 176 | """ 177 | 178 | 179 | class NoStudentNameException(IPythonDoctesterException): 180 | txt = """ 181 | OOPS! We need you to set the ipython_doctester.student_name variable; 182 | please look for it (probably in the first cell in this worksheet) and 183 | enter your name, like 184 | ipython_doctester.student_name = 'Catherine' 185 | ... then hit Shift+Enter to execute that cell, then come back here to 186 | execute this one. 187 | """ 188 | 189 | 190 | def testobj(func): 191 | tests = finder.find(func) 192 | if (not tests) or (not tests[0].examples): 193 | doctest_filename = os.path.join(os.curdir, doctest_path, func.__name__ 194 | ) + '.txt' 195 | try: 196 | with open(doctest_filename) as infile: 197 | func.__doc__ = (func.__doc__ or "") + "\n" + infile.read() 198 | except IOError: 199 | raise NoTestsException 200 | tests = finder.find(func) 201 | if not tests: 202 | raise NoTestsException 203 | if workshop_name and not student_name: 204 | raise NoStudentNameException() 205 | globs = {} # TODO: get the ipython globals? 206 | reporter.__init__() 207 | globs[func.__name__] = func 208 | globs['reporter'] = reporter 209 | for t in tests: 210 | t.globs = globs.copy() 211 | runner.run(t, out=reporter.trap_txt) 212 | reporter.publish() 213 | if workshop_name: 214 | payload = dict(function_name=func.__name__, 215 | failure=reporter.failed, 216 | source=inspect.getsource(func), 217 | workshop_name=workshop_name, 218 | student_name=student_name) 219 | requests.post(docent_url + '/record', data=payload) 220 | 221 | return reporter 222 | 223 | def test(func): 224 | if run_tests: 225 | try: 226 | result = testobj(func) 227 | except (NoStudentNameException, NoTestsException) as e: 228 | IPython.core.display.display(e) 229 | return func 230 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='ipython_doctester', 5 | author='Catherine Devlin', 6 | author_email='catherine.devlin@gmail.com', 7 | version='0.3.0', 8 | url='http://pypi.python.org/pypi/ipython_doctester/', 9 | py_modules = [ 10 | "ipython_doctester", 11 | ], 12 | license='MIT', 13 | description='Run doctests in individual IPython Notebook cells', 14 | long_description=open('README.txt').read(), 15 | install_requires=[ 16 | "ipython >= 1.0", 17 | "pyzmq >= 2.1.4", 18 | "requests", 19 | ] 20 | ) 21 | --------------------------------------------------------------------------------