From 38f1ed6bc30283ce036bc069ca9b3d8d8de28278 Mon Sep 17 00:00:00 2001 From: Michael Mulley Date: Wed, 25 Mar 2015 16:13:52 -0400 Subject: [PATCH 1/3] Remember the source of interactively defined functions - Patches the linecache module to achieve this - inspect.getsource() works for functions defined on the console - tracebacks show code for frames inside interactively defined functions fixes bpython/bpython#229 --- bpython/history.py | 72 ++++++++++++++++++++++++++++++++ bpython/repl.py | 6 ++- bpython/test/test_interpreter.py | 26 +++++++++--- 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/bpython/history.py b/bpython/history.py index e08ec2684..a6f632f08 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -25,6 +25,7 @@ from __future__ import unicode_literals import io +import linecache import os import stat from itertools import islice @@ -232,3 +233,74 @@ def append_reload_and_write(self, s, filename, encoding): # Make sure that entries contains at least one element. If the # file and s are empty, this can occur. self.entries = [''] + +class BPythonLinecache(dict): + """Replaces the cache dict in the standard-library linecache module, + to also remember (in an unerasable way) bpython console input.""" + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.bpython_history = [] + + def is_bpython_filename(self, fname): + try: + return fname.startswith('' diff --git a/bpython/repl.py b/bpython/repl.py index 349cdc00b..d71aba2c3 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -47,7 +47,7 @@ from bpython.clipboard import get_clipboard, CopyFailed from bpython.config import getpreferredencoding from bpython.formatter import Parenthesis -from bpython.history import History +from bpython.history import History, filename_for_console_input from bpython.paste import PasteHelper, PastePinnwand, PasteFailed from bpython.translations import _, ngettext @@ -94,7 +94,7 @@ def __init__(self, locals=None, encoding=None): def reset_running_time(self): self.running_time = 0 - def runsource(self, source, filename='', symbol='single', + def runsource(self, source, filename=None, symbol='single', encode=True): """Execute Python code. @@ -104,6 +104,8 @@ def runsource(self, source, filename='', symbol='single', if not py3 and encode: source = u'# coding: %s\n%s' % (self.encoding, source) source = source.encode(self.encoding) + if filename is None: + filename = filename_for_console_input(source) with self.timer: return code.InteractiveInterpreter.runsource(self, source, filename, symbol) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index a203accbc..49c01d69a 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -17,6 +17,14 @@ pypy = 'PyPy' in sys.version +def _last_console_filename(): + """Returns the last 'filename' used for console input + (as will be displayed in a traceback).""" + import linecache + try: + return '' % (len(linecache.cache.bpython_history) - 1) + except AttributeError: + return '' class TestInterpreter(unittest.TestCase): def test_syntaxerror(self): @@ -30,11 +38,11 @@ def append_to_a(message): i.runsource('1.1.1.1') if pypy: - expected = ' File ' + green('""') + ', line ' + \ + expected = ' File ' + green('"%s"' % _last_console_filename()) + ', line ' + \ bold(magenta('1')) + '\n 1.1.1.1\n ^\n' + \ bold(red('SyntaxError')) + ': ' + cyan('invalid syntax') + '\n' else: - expected = ' File ' + green('""') + ', line ' + \ + expected = ' File ' + green('"%s"' % _last_console_filename()) + ', line ' + \ bold(magenta('1')) + '\n 1.1.1.1\n ^\n' + \ bold(red('SyntaxError')) + ': ' + cyan('invalid syntax') + '\n' @@ -56,7 +64,7 @@ def f(): def g(): return f() - i.runsource('g()') + i.runsource('g()', encode=False) if pypy: global_not_found = "global name 'g' is not defined" @@ -64,8 +72,8 @@ def g(): global_not_found = "name 'g' is not defined" expected = 'Traceback (most recent call last):\n File ' + \ - green('""') + ', line ' + bold(magenta('1')) + ', in ' + \ - cyan('') + '\n' + bold(red('NameError')) + ': ' + \ + green('"%s"' % _last_console_filename()) + ', line ' + bold(magenta('1')) + ', in ' + \ + cyan('') + '\n g()\n' + bold(red('NameError')) + ': ' + \ cyan(global_not_found) + '\n' self.assertMultiLineEqual(str(plain('').join(a)), str(expected)) @@ -106,3 +114,11 @@ def test_runsource_unicode(self): i.runsource("a = u'\xfe'", encode=True) self.assertIsInstance(i.locals['a'], type(u'')) self.assertEqual(i.locals['a'], u"\xfe") + + def test_getsource_works_on_interactively_defined_functions(self): + source = 'def foo(x):\n return x + 1\n' + i = interpreter.Interp() + i.runsource(source) + import inspect + inspected_source = inspect.getsource(i.locals['foo']) + self.assertEquals(inspected_source, source) From 7ca380d12e3e5b56653d5a321991b29dd87f63e7 Mon Sep 17 00:00:00 2001 From: Michael Mulley Date: Wed, 25 Mar 2015 17:40:46 -0400 Subject: [PATCH 2/3] Move linecache code to new file, use super() --- bpython/history.py | 72 ------------------------------------ bpython/patch_linecache.py | 75 ++++++++++++++++++++++++++++++++++++++ bpython/repl.py | 3 +- 3 files changed, 77 insertions(+), 73 deletions(-) create mode 100644 bpython/patch_linecache.py diff --git a/bpython/history.py b/bpython/history.py index a6f632f08..e08ec2684 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -25,7 +25,6 @@ from __future__ import unicode_literals import io -import linecache import os import stat from itertools import islice @@ -233,74 +232,3 @@ def append_reload_and_write(self, s, filename, encoding): # Make sure that entries contains at least one element. If the # file and s are empty, this can occur. self.entries = [''] - -class BPythonLinecache(dict): - """Replaces the cache dict in the standard-library linecache module, - to also remember (in an unerasable way) bpython console input.""" - - def __init__(self, *args, **kwargs): - dict.__init__(self, *args, **kwargs) - self.bpython_history = [] - - def is_bpython_filename(self, fname): - try: - return fname.startswith('' diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py new file mode 100644 index 000000000..9bd935b50 --- /dev/null +++ b/bpython/patch_linecache.py @@ -0,0 +1,75 @@ +import linecache + +class BPythonLinecache(dict): + """Replaces the cache dict in the standard-library linecache module, + to also remember (in an unerasable way) bpython console input.""" + + def __init__(self, *args, **kwargs): + super(BPythonLinecache, self).__init__(*args, **kwargs) + self.bpython_history = [] + + def is_bpython_filename(self, fname): + try: + return fname.startswith('' diff --git a/bpython/repl.py b/bpython/repl.py index d71aba2c3..877bafc53 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -47,8 +47,9 @@ from bpython.clipboard import get_clipboard, CopyFailed from bpython.config import getpreferredencoding from bpython.formatter import Parenthesis -from bpython.history import History, filename_for_console_input +from bpython.history import History from bpython.paste import PasteHelper, PastePinnwand, PasteFailed +from bpython.patch_linecache import filename_for_console_input from bpython.translations import _, ngettext From 92e9b0b3b8b8a528af94a085df5e7a23df1697e2 Mon Sep 17 00:00:00 2001 From: Michael Mulley Date: Wed, 25 Mar 2015 17:46:36 -0400 Subject: [PATCH 3/3] No need to catch this AttributeError in test code --- bpython/test/test_interpreter.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 49c01d69a..43118096f 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals +import linecache import sys try: @@ -20,11 +21,7 @@ def _last_console_filename(): """Returns the last 'filename' used for console input (as will be displayed in a traceback).""" - import linecache - try: - return '' % (len(linecache.cache.bpython_history) - 1) - except AttributeError: - return '' + return '' % (len(linecache.cache.bpython_history) - 1) class TestInterpreter(unittest.TestCase): def test_syntaxerror(self):