From 4cc39129854bc545c6c92aaca69d995e4e666600 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Mon, 25 Aug 2014 13:42:49 -0400
Subject: [PATCH 0001/1650] F3 to edit current config file
Remaining to do: figure out how to get default config
somewhere that bpython can find it (actually install it somewhere)
---
bpython/config.py | 4 ++++
bpython/curtsiesfrontend/repl.py | 2 ++
bpython/repl.py | 35 ++++++++++++++++++++++++++++++++
3 files changed, 41 insertions(+)
diff --git a/bpython/config.py b/bpython/config.py
index 7a21becfe..df27894b3 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -69,6 +69,7 @@ def loadini(struct, configfile):
'down_one_line': 'C-n',
'exit': '',
'external_editor': 'F7',
+ 'edit_config': 'F3',
'edit_current_block': 'C-x',
'help': 'F1',
'last_output': 'F9',
@@ -99,6 +100,8 @@ def loadini(struct, configfile):
"%s\n" % default_config_path())
sys.exit(1)
+ struct.config_path = config_path
+
struct.dedent_after = config.getint('general', 'dedent_after')
struct.tab_length = config.getint('general', 'tab_length')
struct.auto_display_list = config.getboolean('general',
@@ -131,6 +134,7 @@ def loadini(struct, configfile):
struct.delete_key = config.get('keyboard', 'delete')
struct.exit_key = config.get('keyboard', 'exit')
struct.last_output_key = config.get('keyboard', 'last_output')
+ struct.edit_config_key = config.get('keyboard', 'edit_config')
struct.edit_current_block_key = config.get('keyboard', 'edit_current_block')
struct.external_editor_key = config.get('keyboard', 'external_editor')
struct.help_key = config.get('keyboard', 'help')
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 0bac4f2e3..1f39e7699 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -491,6 +491,8 @@ def process_key_event(self, e):
greenlet.greenlet(self.pastebin).switch()
elif e in key_dispatch[self.config.external_editor_key]:
self.send_session_to_external_editor()
+ elif e in key_dispatch[self.config.edit_config_key]:
+ greenlet.greenlet(self.edit_config).switch()
#TODO add PAD keys hack as in bpython.cli
elif e in key_dispatch[self.config.edit_current_block_key]:
self.send_current_block_to_external_editor()
diff --git a/bpython/repl.py b/bpython/repl.py
index 64b14566a..5256f011e 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -1022,6 +1022,41 @@ def send_to_external_editor(self, text, filename=None):
else:
return text
+ def open_in_external_editor(self, filename):
+ editor_args = shlex.split(self.config.editor)
+ if subprocess.call(editor_args + [filename]) == 0:
+ return True
+ return False
+
+ def edit_config(self):
+ #TODO put default-config somewhere accessible to this code
+ DEFAULT = ("[general]\n"
+ "syntax = True\n"
+ "[keyboard]\n"
+ "pastebin = F8\n"
+ "save = C-s\n")
+
+ open('a.txt', 'w').write(repr(self.config.config_path))
+ if not (os.path.isfile(self.config.config_path)):
+ if self.interact.confirm(_("Config file does not exist - create new from default? (y/N)")):
+ try:
+ containing_dir = os.path.dirname(os.path.abspath(self.config.config_path))
+ if not os.path.exists(containing_dir):
+ os.makedirs(containing_dir)
+ with open(self.config.config_path, 'w') as f:
+ f.write(DEFAULT)
+ except (IOError, OSError) as e:
+ self.interact.notify('error creating file: %r' % e)
+ raise e
+ return False
+ else:
+ return False
+
+ if self.open_in_external_editor(self.config.config_path):
+ self.interact.notify('bpython config file edited. Restart bpython for changes to take effect.')
+ else:
+ self.interact.notify('error editing config file')
+
def next_indentation(line, tab_length):
"""Given a code line, return the indentation of the next line."""
From 60a0687bcb0fc9c8489557f24ac754990de35ed4 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 26 Aug 2014 14:42:58 -0700
Subject: [PATCH 0002/1650] manual_readline refactor
---
bpython/curtsiesfrontend/interaction.py | 6 +-
bpython/curtsiesfrontend/manual_readline.py | 198 +++++++++++++++-----
bpython/curtsiesfrontend/repl.py | 30 ++-
bpython/test/test_manual_readline.py | 42 +++++
4 files changed, 212 insertions(+), 64 deletions(-)
diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py
index e67667569..aa2181e07 100644
--- a/bpython/curtsiesfrontend/interaction.py
+++ b/bpython/curtsiesfrontend/interaction.py
@@ -4,7 +4,7 @@
from bpython.repl import Interaction as BpythonInteraction
-from bpython.curtsiesfrontend.manual_readline import char_sequences as rl_char_sequences
+from bpython.curtsiesfrontend.manual_readline import edit_keys
class StatusBar(BpythonInteraction):
"""StatusBar and Interaction for Repl
@@ -68,8 +68,8 @@ def process_event(self, e):
elif isinstance(e, events.PasteEvent):
for ee in e.events:
self.add_normal_character(ee if len(ee) == 1 else ee[-1]) #strip control seq
- elif e in rl_char_sequences:
- self.cursor_offset_in_line, self._current_line = rl_char_sequences[e](self.cursor_offset_in_line, self._current_line)
+ elif e in edit_keys:
+ self.cursor_offset_in_line, self._current_line = edit_keys[e](self.cursor_offset_in_line, self._current_line)
elif e == "":
raise KeyboardInterrupt()
elif e == "":
diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py
index 50ab4f6e4..6c8f04410 100644
--- a/bpython/curtsiesfrontend/manual_readline.py
+++ b/bpython/curtsiesfrontend/manual_readline.py
@@ -1,57 +1,151 @@
-"""implementations of simple readline control sequences
+"""implementations of simple readline edit operations
just the ones that fit the model of transforming the current line
and the cursor location
-in the order of description at http://www.bigsmoke.us/readline/shortcuts"""
+based on http://www.bigsmoke.us/readline/shortcuts"""
import re
-char_sequences = {}
+import inspect
INDENT = 4
#TODO Allow user config of keybindings for these actions
-def on(seq):
- def add_to_char_sequences(func):
- char_sequences[seq] = func
- return func
- return add_to_char_sequences
-
-@on('')
-@on('')
+class AbstractEdits(object):
+
+ default_kwargs = {
+ 'line': 'hello world',
+ 'cursor_offset': 5,
+ 'cut_buffer': 'there',
+ 'indent': 4,
+ }
+
+ def __contains__(self, key):
+ try:
+ self[key]
+ except KeyError:
+ return False
+ else:
+ return True
+
+ def add(self, key, func):
+ if key in self:
+ raise ValueError('key %r already has a mapping' % (key,))
+ params = inspect.getargspec(func)[0]
+ args = dict((k, v) for k, v in self.default_kwargs.items() if k in params)
+ r = func(**args)
+ if len(r) == 2:
+ self.simple_edits[key] = func
+ elif len(r) == 3:
+ self.cut_buffer_edits[key] = func
+ else:
+ raise ValueError('return type of function %r not recognized' % (func,))
+
+ def add_config_attr(self, config_attr, func):
+ if config_attr in self.awaiting_config:
+ raise ValueError('config attrribute %r already has a mapping' % (config_attr,))
+ self.awaiting_config[config_attr] = func
+
+ def call(self, key, **kwargs):
+ func = self[key]
+ params = inspect.getargspec(func)[0]
+ args = dict((k, v) for k, v in kwargs.items() if k in params)
+ return func(**args)
+
+ def __getitem__(self, key):
+ if key in self.simple_edits: return self.simple_edits[key]
+ if key in self.cut_buffer_edits: return self.cut_buffer_edits[key]
+ raise KeyError("key %r not mapped" % (key,))
+
+
+class UnconfiguredEdits(AbstractEdits):
+ """Maps key to edit functions, and bins them by what parameters they take.
+
+ Only functions with specific signatures can be added:
+ * func(**kwargs) -> cursor_offset, line
+ * func(**kwargs) -> cursor_offset, line, cut_buffer
+ where kwargs are in among the keys of Edits.default_kwargs
+ These functions will be run to determine their return type, so no side effects!
+
+ More concrete Edits instances can be created by applying a config with
+ Edits.mapping_with_config() - this creates a new Edits instance
+ that uses a config file to assign config_attr bindings.
+
+ Keys can't be added twice, config attributes can't be added twice.
+ """
+
+ def __init__(self):
+ self.simple_edits = {}
+ self.cut_buffer_edits = {}
+ self.awaiting_config = {}
+
+ def mapping_with_config(self, config, key_dispatch):
+ """Creates a new mapping object by applying a config object"""
+ return ConfiguredEdits(self.simple_edits, self.cut_buffer_edits,
+ self.awaiting_config, config, key_dispatch)
+
+ def on(self, key=None, config=None):
+ if (key is None and config is None or
+ key is not None and config is not None):
+ raise ValueError("Must use exactly one of key, config")
+ if key is not None:
+ def add_to_keybinds(func):
+ self.add(key, func)
+ return func
+ return add_to_keybinds
+ else:
+ def add_to_config(func):
+ self.add_config_attr(config, func)
+ return func
+ return add_to_config
+
+class ConfiguredEdits(AbstractEdits):
+ def __init__(self, simple_edits, cut_buffer_edits, awaiting_config, config, key_dispatch):
+ self.simple_edits = dict(simple_edits)
+ self.cut_buffer_edits = dict(cut_buffer_edits)
+ for attr, func in awaiting_config.items():
+ super(ConfiguredEdits, self).add(key_dispatch[getattr(config, attr)], func)
+
+ def add_config_attr(self, config_attr, func):
+ raise NotImplementedError("Config already set on this mapping")
+
+ def add(self, key, func):
+ raise NotImplementedError("Config already set on this mapping")
+
+edit_keys = UnconfiguredEdits()
+
+# Because the edits.on decorator runs the functions, functions which depend
+# on other functions must be declared after their dependencies
+
+@edit_keys.on('')
+@edit_keys.on('')
def left_arrow(cursor_offset, line):
return max(0, cursor_offset - 1), line
-@on('')
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
def right_arrow(cursor_offset, line):
return min(len(line), cursor_offset + 1), line
-@on('')
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
def beginning_of_line(cursor_offset, line):
return 0, line
-@on('')
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
def end_of_line(cursor_offset, line):
return len(line), line
-@on('')
-@on('')
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
+@edit_keys.on('')
def forward_word(cursor_offset, line):
patt = r"\S\s"
match = re.search(patt, line[cursor_offset:]+' ')
delta = match.end() - 1 if match else 0
return (cursor_offset + delta, line)
-@on('')
-@on('')
-@on('')
-def back_word(cursor_offset, line):
- return (last_word_pos(line[:cursor_offset]), line)
-
def last_word_pos(string):
"""returns the start index of the last word of given string"""
patt = r'\S\s'
@@ -59,13 +153,20 @@ def last_word_pos(string):
index = match and len(string) - match.end() + 1
return index or 0
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
+@edit_keys.on('')
+def back_word(cursor_offset, line):
+ return (last_word_pos(line[:cursor_offset]), line)
+
+@edit_keys.on('')
def delete(cursor_offset, line):
return (cursor_offset,
line[:cursor_offset] + line[cursor_offset+1:])
-@on('')
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
+@edit_keys.on(config='delete_key')
def backspace(cursor_offset, line):
if cursor_offset == 0:
return cursor_offset, line
@@ -76,32 +177,30 @@ def backspace(cursor_offset, line):
return (cursor_offset - 1,
line[:cursor_offset - 1] + line[cursor_offset:])
-@on('')
+@edit_keys.on('')
+@edit_keys.on(config='clear_line_key')
def delete_from_cursor_back(cursor_offset, line):
return 0, line[cursor_offset:]
-@on('')
-def delete_from_cursor_forward(cursor_offset, line):
- return cursor_offset, line[:cursor_offset]
-
-@on('') # option-d
+@edit_keys.on('') # option-d
def delete_rest_of_word(cursor_offset, line):
m = re.search(r'\w\b', line[cursor_offset:])
if not m:
return cursor_offset, line
return cursor_offset, line[:cursor_offset] + line[m.start()+cursor_offset+1:]
-@on('')
+@edit_keys.on('')
+@edit_keys.on(config='clear_word_key')
def delete_word_to_cursor(cursor_offset, line):
matches = list(re.finditer(r'\s\S', line[:cursor_offset]))
start = matches[-1].start()+1 if matches else 0
return start, line[:start] + line[cursor_offset:]
-@on('')
+@edit_keys.on('')
def yank_prev_prev_killed_text(cursor_offset, line):
return cursor_offset, line #TODO Not implemented
-@on('')
+@edit_keys.on('')
def transpose_character_before_cursor(cursor_offset, line):
return (min(len(line), cursor_offset + 1),
line[:cursor_offset-1] +
@@ -109,26 +208,30 @@ def transpose_character_before_cursor(cursor_offset, line):
line[cursor_offset - 1] +
line[cursor_offset+1:])
-@on('')
+@edit_keys.on('')
def transpose_word_before_cursor(cursor_offset, line):
return cursor_offset, line #TODO Not implemented
# bonus functions (not part of readline)
-@on('')
+@edit_keys.on('')
def delete_line(cursor_offset, line):
return 0, ""
-@on('')
+@edit_keys.on('')
def uppercase_next_word(cursor_offset, line):
return cursor_offset, line #TODO Not implemented
-@on('')
+@edit_keys.on('')
+def delete_from_cursor_forward(cursor_offset, line):
+ return cursor_offset, line[:cursor_offset]
+
+@edit_keys.on('')
def titlecase_next_word(cursor_offset, line):
return cursor_offset, line #TODO Not implemented
-@on('')
-@on('')
+@edit_keys.on('')
+@edit_keys.on('')
def delete_word_from_cursor_back(cursor_offset, line):
"""Whatever my option-delete does in bash on my mac"""
if not line:
@@ -138,10 +241,3 @@ def delete_word_from_cursor_back(cursor_offset, line):
return starts[-1], line[:starts[-1]] + line[cursor_offset:]
return cursor_offset, line
-def get_updated_char_sequences(key_dispatch, config):
- updated_char_sequences = dict(char_sequences)
- updated_char_sequences[key_dispatch[config.delete_key]] = backspace
- updated_char_sequences[key_dispatch[config.clear_word_key]] = delete_word_to_cursor
- updated_char_sequences[key_dispatch[config.clear_line_key]] = delete_from_cursor_back
- return updated_char_sequences
-
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 0bac4f2e3..f83de19b1 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -41,8 +41,7 @@
from bpython.curtsiesfrontend.coderunner import CodeRunner, FakeOutput
from bpython.curtsiesfrontend.filewatch import ModuleChangedEventHandler
from bpython.curtsiesfrontend.interaction import StatusBar
-from bpython.curtsiesfrontend.manual_readline import char_sequences as rl_char_sequences
-from bpython.curtsiesfrontend.manual_readline import get_updated_char_sequences
+from bpython.curtsiesfrontend.manual_readline import edit_keys
#TODO other autocomplete modes (also fix in other bpython implementations)
@@ -72,7 +71,7 @@
class FakeStdin(object):
"""Stdin object user code references so sys.stdin.read() asked user for interactive input"""
- def __init__(self, coderunner, repl, updated_rl_char_sequences=None):
+ def __init__(self, coderunner, repl, configured_edit_keys=None):
self.coderunner = coderunner
self.repl = repl
self.has_focus = False # whether FakeStdin receives keypress events
@@ -80,10 +79,10 @@ def __init__(self, coderunner, repl, updated_rl_char_sequences=None):
self.cursor_offset = 0
self.old_num_lines = 0
self.readline_results = []
- if updated_rl_char_sequences:
- self.rl_char_sequences = updated_rl_char_sequences
+ if configured_edit_keys:
+ self.rl_char_sequences = configured_edit_keys
else:
- self.rl_char_sequences = rl_char_sequences
+ self.rl_char_sequences = edit_keys
def process_event(self, e):
assert self.has_focus
@@ -267,7 +266,7 @@ def smarter_request_reload(desc):
if config.curtsies_fill_terminal else ''),
refresh_request=self.request_refresh
)
- self.rl_char_sequences = get_updated_char_sequences(key_dispatch, config)
+ self.edit_keys = edit_keys.mapping_with_config(config, key_dispatch)
logger.debug("starting parent init")
super(Repl, self).__init__(interp, config)
#TODO bring together all interactive stuff - including current directory in path?
@@ -296,7 +295,7 @@ def smarter_request_reload(desc):
self.coderunner = CodeRunner(self.interp, self.request_refresh)
self.stdout = FakeOutput(self.coderunner, self.send_to_stdout)
self.stderr = FakeOutput(self.coderunner, self.send_to_stderr)
- self.stdin = FakeStdin(self.coderunner, self, self.rl_char_sequences)
+ self.stdin = FakeStdin(self.coderunner, self, self.edit_keys)
self.request_paint_to_clear_screen = False # next paint should clear screen
self.last_events = [None] * 50
@@ -459,8 +458,12 @@ def process_key_event(self, e):
self.up_one_line()
elif e in ("",) + key_dispatch[self.config.down_one_line_key]:
self.down_one_line()
- elif e in self.rl_char_sequences:
- self.cursor_offset, self.current_line = self.rl_char_sequences[e](self.cursor_offset, self.current_line)
+ elif e in self.edit_keys:
+ self.cursor_offset, self.current_line = self.edit_keys[e](self.cursor_offset, self.current_line)
+ elif e in key_dispatch[self.config.cut_to_buffer_key]:
+ self.cut_to_buffer()
+ elif e in key_dispatch[self.config.yank_from_buffer_key]:
+ self.yank_from_buffer()
elif e in key_dispatch[self.config.reimport_key]:
self.clear_modules_and_reevaluate()
elif e in key_dispatch[self.config.toggle_file_watch_key]:
@@ -558,6 +561,13 @@ def on_control_d(self):
else:
self.current_line = self.current_line[:self.cursor_offset] + self.current_line[self.cursor_offset+1:]
+ def cut_to_buffer(self):
+ self.cut_buffer = self.current_line[self.cursor_offset:]
+ self.current_line = self.current_line[:self.cursor_offset]
+
+ def yank_from_buffer(self):
+ pass
+
def up_one_line(self):
self.rl_history.enter(self.current_line)
self._set_current_line(self.rl_history.back(False, search=self.config.curtsies_right_arrow_completion),
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index 004037c0b..cb7e242fe 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -1,3 +1,6 @@
+
+from collections import namedtuple
+
from bpython.curtsiesfrontend.manual_readline import *
import unittest
@@ -202,5 +205,44 @@ def test_delete_word_from_cursor_back(self):
"asd;|",
"|"], delete_word_from_cursor_back)
+class TestEdits(unittest.TestCase):
+
+ def setUp(self):
+ self.edits = UnconfiguredEdits()
+
+ def test_seq(self):
+ f = lambda cursor_offset, line: ('hi', 2)
+ self.edits.add('a', f)
+ self.assertTrue('a' in self.edits)
+ self.assertEqual(self.edits['a'], f)
+ self.assertEqual(self.edits.call('a', cursor_offset=3, line='hello'),
+ ('hi', 2))
+ self.assertRaises(KeyError, self.edits.__getitem__, 'b')
+ self.assertRaises(KeyError, self.edits.call, 'b')
+
+ def test_functions_with_bad_signatures(self):
+ pass #TODO
+
+ def test_functions_with_bad_return_values(self):
+ pass #TODO
+
+ def test_config(self):
+ f = lambda cursor_offset, line: ('hi', 2)
+ g = lambda cursor_offset, line: ('hey', 3)
+ self.edits.add_config_attr('att', f)
+ self.assertFalse('att' in self.edits)
+ class config: att = 'c'
+ key_dispatch = {'c': 'c'}
+ configured_edits = self.edits.mapping_with_config(config, key_dispatch)
+ self.assertTrue(configured_edits.__contains__, 'c')
+ self.assertFalse('c' in self.edits)
+ self.assertRaises(NotImplementedError,
+ configured_edits.add_config_attr, 'att2', g)
+ self.assertRaises(NotImplementedError,
+ configured_edits.add, 'd', g)
+ self.assertEqual(configured_edits.call('c', cursor_offset=5, line='asfd'),
+ ('hi', 2))
+
+
if __name__ == '__main__':
unittest.main()
From c92b043d711185100533f0432818d8cf72ba7c31 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 26 Aug 2014 16:21:36 -0700
Subject: [PATCH 0003/1650] yank working!
---
bpython/curtsiesfrontend/manual_readline.py | 58 ++++++++++++++++-----
bpython/curtsiesfrontend/repl.py | 25 ++++++---
bpython/test/test_manual_readline.py | 5 ++
3 files changed, 70 insertions(+), 18 deletions(-)
diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py
index 6c8f04410..dd51d590e 100644
--- a/bpython/curtsiesfrontend/manual_readline.py
+++ b/bpython/curtsiesfrontend/manual_readline.py
@@ -17,7 +17,6 @@ class AbstractEdits(object):
'line': 'hello world',
'cursor_offset': 5,
'cut_buffer': 'there',
- 'indent': 4,
}
def __contains__(self, key):
@@ -28,15 +27,22 @@ def __contains__(self, key):
else:
return True
- def add(self, key, func):
+ def add(self, key, func, overwrite=False):
if key in self:
- raise ValueError('key %r already has a mapping' % (key,))
+ if overwrite:
+ del self[key]
+ else:
+ raise ValueError('key %r already has a mapping' % (key,))
params = inspect.getargspec(func)[0]
args = dict((k, v) for k, v in self.default_kwargs.items() if k in params)
r = func(**args)
if len(r) == 2:
+ if hasattr(func, 'kills'):
+ raise ValueError('function %r returns two values, but has a kills attribute' % (func,))
self.simple_edits[key] = func
elif len(r) == 3:
+ if not hasattr(func, 'kills'):
+ raise ValueError('function %r returns three values, but has no kills attribute' % (func,))
self.cut_buffer_edits[key] = func
else:
raise ValueError('return type of function %r not recognized' % (func,))
@@ -52,11 +58,20 @@ def call(self, key, **kwargs):
args = dict((k, v) for k, v in kwargs.items() if k in params)
return func(**args)
+ def call_without_cut(self, key, **kwargs):
+ """Looks up the function and calls it, returning only line and cursor offset"""
+ r = self.call_for_two(key, **kwargs)
+ return r[:2]
+
def __getitem__(self, key):
if key in self.simple_edits: return self.simple_edits[key]
if key in self.cut_buffer_edits: return self.cut_buffer_edits[key]
raise KeyError("key %r not mapped" % (key,))
+ def __delitem__(self, key):
+ if key in self.simple_edits: del self.simple_edits[key]
+ elif key in self.cut_buffer_edits: del self.cut_buffer_edits[key]
+ else: raise KeyError("key %r not mapped" % (key,))
class UnconfiguredEdits(AbstractEdits):
"""Maps key to edit functions, and bins them by what parameters they take.
@@ -104,7 +119,8 @@ def __init__(self, simple_edits, cut_buffer_edits, awaiting_config, config, key_
self.simple_edits = dict(simple_edits)
self.cut_buffer_edits = dict(cut_buffer_edits)
for attr, func in awaiting_config.items():
- super(ConfiguredEdits, self).add(key_dispatch[getattr(config, attr)], func)
+ for key in key_dispatch[getattr(config, attr)]:
+ super(ConfiguredEdits, self).add(key, func, overwrite=True)
def add_config_attr(self, config_attr, func):
raise NotImplementedError("Config already set on this mapping")
@@ -117,6 +133,14 @@ def add(self, key, func):
# Because the edits.on decorator runs the functions, functions which depend
# on other functions must be declared after their dependencies
+def kills_behind(func):
+ func.kills = 'behind'
+ return func
+
+def kills_ahead(func):
+ func.kills = 'ahead'
+ return func
+
@edit_keys.on('')
@edit_keys.on('')
def left_arrow(cursor_offset, line):
@@ -183,22 +207,29 @@ def delete_from_cursor_back(cursor_offset, line):
return 0, line[cursor_offset:]
@edit_keys.on('') # option-d
+@kills_ahead
def delete_rest_of_word(cursor_offset, line):
m = re.search(r'\w\b', line[cursor_offset:])
if not m:
- return cursor_offset, line
- return cursor_offset, line[:cursor_offset] + line[m.start()+cursor_offset+1:]
+ return cursor_offset, line, ''
+ return (cursor_offset, line[:cursor_offset] + line[m.start()+cursor_offset+1:],
+ line[cursor_offset:m.start()+cursor_offset+1])
@edit_keys.on('')
@edit_keys.on(config='clear_word_key')
+@kills_behind
def delete_word_to_cursor(cursor_offset, line):
matches = list(re.finditer(r'\s\S', line[:cursor_offset]))
start = matches[-1].start()+1 if matches else 0
- return start, line[:start] + line[cursor_offset:]
+ return start, line[:start] + line[cursor_offset:], line[start:cursor_offset]
@edit_keys.on('')
-def yank_prev_prev_killed_text(cursor_offset, line):
- return cursor_offset, line #TODO Not implemented
+def yank_prev_prev_killed_text(cursor_offset, line, cut_buffer): #TODO not implemented - just prev
+ return cursor_offset+len(cut_buffer), line[:cursor_offset] + cut_buffer + line[cursor_offset:]
+
+@edit_keys.on(config='yank_from_buffer_key')
+def yank_prev_killed_text(cursor_offset, line, cut_buffer):
+ return cursor_offset+len(cut_buffer), line[:cursor_offset] + cut_buffer + line[cursor_offset:]
@edit_keys.on('')
def transpose_character_before_cursor(cursor_offset, line):
@@ -223,8 +254,9 @@ def uppercase_next_word(cursor_offset, line):
return cursor_offset, line #TODO Not implemented
@edit_keys.on('')
+@kills_ahead
def delete_from_cursor_forward(cursor_offset, line):
- return cursor_offset, line[:cursor_offset]
+ return cursor_offset, line[:cursor_offset], line[cursor_offset:]
@edit_keys.on('')
def titlecase_next_word(cursor_offset, line):
@@ -232,12 +264,14 @@ def titlecase_next_word(cursor_offset, line):
@edit_keys.on('')
@edit_keys.on('')
+@kills_behind
def delete_word_from_cursor_back(cursor_offset, line):
"""Whatever my option-delete does in bash on my mac"""
if not line:
return cursor_offset, line
starts = [m.start() for m in list(re.finditer(r'\b\w', line)) if m.start() < cursor_offset]
if starts:
- return starts[-1], line[:starts[-1]] + line[cursor_offset:]
- return cursor_offset, line
+ return starts[-1], line[:starts[-1]] + line[cursor_offset:], line[starts[-1]:cursor_offset]
+ return cursor_offset, line, ''
+
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index f83de19b1..2ee25dc27 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -458,12 +458,17 @@ def process_key_event(self, e):
self.up_one_line()
elif e in ("",) + key_dispatch[self.config.down_one_line_key]:
self.down_one_line()
- elif e in self.edit_keys:
- self.cursor_offset, self.current_line = self.edit_keys[e](self.cursor_offset, self.current_line)
+ elif e in ("",):
+ self.on_control_d()
+ elif e in self.edit_keys.cut_buffer_edits:
+ self.readline_kill(e)
+ elif e in self.edit_keys.simple_edits:
+ self.cursor_offset, self.current_line = self.edit_keys.call(e,
+ cursor_offset=self.cursor_offset,
+ line=self.current_line,
+ cut_buffer=self.cut_buffer)
elif e in key_dispatch[self.config.cut_to_buffer_key]:
self.cut_to_buffer()
- elif e in key_dispatch[self.config.yank_from_buffer_key]:
- self.yank_from_buffer()
elif e in key_dispatch[self.config.reimport_key]:
self.clear_modules_and_reevaluate()
elif e in key_dispatch[self.config.toggle_file_watch_key]:
@@ -476,8 +481,6 @@ def process_key_event(self, e):
self.pager(self.help_text())
elif e in key_dispatch[self.config.suspend_key]:
raise SystemExit()
- elif e in ("",):
- self.on_control_d()
elif e in key_dispatch[self.config.exit_key]:
raise SystemExit()
elif e in ("\n", "\r", "", "", ""):
@@ -504,6 +507,16 @@ def process_key_event(self, e):
else:
self.add_normal_character(e)
+ def readline_kill(self, e):
+ func = self.edit_keys[e]
+ self.cursor_offset, self.current_line, cut = func(self.cursor_offset, self.current_line)
+ if self.last_events[-2] == e: # consecutive kill commands are cumulative
+ if func.kills == 'ahead': self.cut_buffer += cut
+ elif func.kills == 'behind': self.cut_buffer = cut + self.cut_buffer
+ else: raise ValueError("cut had value other than 'ahead' or 'behind'")
+ else:
+ self.cut_buffer = cut
+
def on_enter(self, insert_into_history=True):
self.cursor_offset = -1 # so the cursor isn't touching a paren
self.unhighlight_paren() # in unhighlight_paren
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index cb7e242fe..8c609d575 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -243,6 +243,11 @@ class config: att = 'c'
self.assertEqual(configured_edits.call('c', cursor_offset=5, line='asfd'),
('hi', 2))
+ def test_actual(self):
+ class config: att = 'c'
+ key_dispatch = {'c': 'c'}
+ configured_edits = self.edits.mapping_with_config(config, key_dispatch)
+
if __name__ == '__main__':
unittest.main()
From 46bc5125a4cf0eebf0350c22bfac0d68566add7f Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Wed, 27 Aug 2014 14:23:07 -0700
Subject: [PATCH 0004/1650] Make sample config used as template for new config
file
---
bpython/repl.py | 15 +++++----------
sample-config => bpython/sample-config | 0
setup.py | 2 +-
3 files changed, 6 insertions(+), 11 deletions(-)
rename sample-config => bpython/sample-config (100%)
diff --git a/bpython/repl.py b/bpython/repl.py
index 5256f011e..11e9f9a28 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -1029,25 +1029,20 @@ def open_in_external_editor(self, filename):
return False
def edit_config(self):
- #TODO put default-config somewhere accessible to this code
- DEFAULT = ("[general]\n"
- "syntax = True\n"
- "[keyboard]\n"
- "pastebin = F8\n"
- "save = C-s\n")
-
- open('a.txt', 'w').write(repr(self.config.config_path))
if not (os.path.isfile(self.config.config_path)):
if self.interact.confirm(_("Config file does not exist - create new from default? (y/N)")):
try:
+ bpython_dir, script_name = os.path.split(__file__)
+ with open(os.path.join(bpython_dir, "sample-config")) as f:
+ default_config = f.read()
+
containing_dir = os.path.dirname(os.path.abspath(self.config.config_path))
if not os.path.exists(containing_dir):
os.makedirs(containing_dir)
with open(self.config.config_path, 'w') as f:
- f.write(DEFAULT)
+ f.write(default_config)
except (IOError, OSError) as e:
self.interact.notify('error creating file: %r' % e)
- raise e
return False
else:
return False
diff --git a/sample-config b/bpython/sample-config
similarity index 100%
rename from sample-config
rename to bpython/sample-config
diff --git a/setup.py b/setup.py
index 3a050ff63..8de734e8c 100755
--- a/setup.py
+++ b/setup.py
@@ -184,7 +184,7 @@ def initialize_options(self):
packages = packages,
data_files = data_files,
package_data = {
- 'bpython': ['logo.png'],
+ 'bpython': ['logo.png', 'sample-config'],
'bpython.translations': mo_files,
'bpython.test': ['test.config', 'test.theme']
},
From 434313273b235d8c38379a41c324323e7e7a872f Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Wed, 27 Aug 2014 14:26:34 -0700
Subject: [PATCH 0005/1650] reference config docs and fix typo in config
---
bpython/sample-config | 3 +++
doc/sphinx/source/configuration-options.rst | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/bpython/sample-config b/bpython/sample-config
index d031556fe..bcb2c0901 100644
--- a/bpython/sample-config
+++ b/bpython/sample-config
@@ -3,6 +3,9 @@
# By default bpython will look for $XDG_CONFIG_HOME/bpython/config
# ($XDG_CONFIG_HOME defaults to ~/.config) or you can specify a file with the
# --config option on the command line
+#
+# see http://docs.bpython-interpreter.org/configuration.html
+# for all configurable options
# General section tag
[general]
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index c25b38a57..1aa79b4af 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -159,7 +159,7 @@ Valid keys are:
pastebin
^^^^^^^^
-Default:
+Default: F8
last_output
^^^^^^^^^^^
From 812d7ee48457103a3a49624f11c0666305be0444 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 28 Aug 2014 14:40:08 -0700
Subject: [PATCH 0006/1650] fix tests to deal with kill functions
---
bpython/test/test_manual_readline.py | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index 8c609d575..96a695310 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -142,11 +142,11 @@ def test_delete_from_cursor_forward(self):
line = "everything after this will be deleted"
pos = line.find("this")
expected = (pos, "everything after ")
- result = delete_from_cursor_forward(line.find("this"), line)
+ result = delete_from_cursor_forward(line.find("this"), line)[:-1]
self.assertEquals(expected, result)
def test_delete_rest_of_word(self):
- self.try_stages(['z|s;df asdf d s;a;a',
+ self.try_stages_kill(['z|s;df asdf d s;a;a',
'z|;df asdf d s;a;a',
'z| asdf d s;a;a',
'z| d s;a;a',
@@ -156,7 +156,7 @@ def test_delete_rest_of_word(self):
'z|'], delete_rest_of_word)
def test_delete_word_to_cursor(self):
- self.try_stages([
+ self.try_stages_kill([
' a;d sdf ;a;s;d; fjksald|a',
' a;d sdf ;a;s;d; |a',
' a;d sdf |a',
@@ -179,6 +179,15 @@ def try_stages(self, strings, func):
for (initial_pos, initial), (final_pos, final) in zip(stages[:-1], stages[1:]):
self.assertEquals(func(initial_pos, initial), (final_pos, final))
+ def try_stages_kill(self, strings, func):
+ if not all('|' in s for s in strings):
+ raise ValueError("Need to use '|' to specify cursor")
+
+ stages = [(s.index('|'), s.replace('|', '')) for s in strings]
+ for (initial_pos, initial), (final_pos, final) in zip(stages[:-1], stages[1:]):
+ self.assertEquals(func(initial_pos, initial)[:-1], (final_pos, final))
+
+
def test_transpose_character_before_cursor(self):
self.try_stages(["as|df asdf",
"ads|f asdf",
@@ -194,7 +203,7 @@ def test_backspace(self):
self.assertEquals(backspace(3, 'as '), (2, 'as'))
def test_delete_word_from_cursor_back(self):
- self.try_stages([
+ self.try_stages_kill([
"asd;fljk asd;lfjas;dlkfj asdlk jasdf;ljk|",
"asd;fljk asd;lfjas;dlkfj asdlk jasdf;|",
"asd;fljk asd;lfjas;dlkfj asdlk |",
From fc1cf63e3d2c78d9ed21beef60f692b9f2bd1147 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 28 Aug 2014 15:05:06 -0700
Subject: [PATCH 0007/1650] fix send_current_block_to_external_editor
---
bpython/curtsiesfrontend/repl.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 2ee25dc27..ef294c433 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -594,7 +594,7 @@ def down_one_line(self):
self._set_cursor_offset(len(self.current_line), reset_rl_history=False)
def process_simple_keypress(self, e):
- if e in (u"", u"", u""):
+ if e in (u"", u"", u"", u"\n", u"\r"): # '\n' necessary for pastes
self.on_enter()
while self.fake_refresh_requested:
self.fake_refresh_requested = False
@@ -607,7 +607,7 @@ def process_simple_keypress(self, e):
self.add_normal_character(e)
def send_current_block_to_external_editor(self, filename=None):
- text = self.send_to_external_editor(self.get_current_block())
+ text = self.send_to_external_editor(self.get_current_block().encode('utf8'))
lines = [line for line in text.split('\n')]
while lines and not lines[-1].split():
lines.pop()
@@ -616,7 +616,6 @@ def send_current_block_to_external_editor(self, filename=None):
with self.in_paste_mode():
for e in events:
self.process_simple_keypress(e)
- self.current_line = ''
self.cursor_offset = len(self.current_line)
def send_session_to_external_editor(self, filename=None):
From e318b6bbd96aa542c971aa8fbcfd23e8eec87923 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 28 Aug 2014 10:25:35 -0700
Subject: [PATCH 0008/1650] first draft of incremental search (ctrl-r/s)
---
bpython/curtsiesfrontend/interaction.py | 1 +
bpython/curtsiesfrontend/repl.py | 78 ++++++++++++++++++++-----
bpython/test/test_curtsies_painting.py | 4 +-
3 files changed, 68 insertions(+), 15 deletions(-)
diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py
index aa2181e07..c58eae11b 100644
--- a/bpython/curtsiesfrontend/interaction.py
+++ b/bpython/curtsiesfrontend/interaction.py
@@ -51,6 +51,7 @@ def has_focus(self):
return self.in_prompt or self.in_confirm or self.waiting_for_refresh
def message(self, msg):
+ """Sets a temporary message"""
self.message_start_time = time.time()
self._message = msg
self.refresh_request(time.time() + self.message_time)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index ef294c433..e45438f79 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -298,16 +298,19 @@ def smarter_request_reload(desc):
self.stdin = FakeStdin(self.coderunner, self, self.edit_keys)
self.request_paint_to_clear_screen = False # next paint should clear screen
- self.last_events = [None] * 50
- self.presentation_mode = False
- self.paste_mode = False
- self.current_match = None
- self.list_win_visible = False
- self.watching_files = False
+ self.last_events = [None] * 50 # some commands act differently based on the prev event
+ # this list doesn't include instances of event.Event,
+ # only keypress-type events (no refresh screen events etc.)
+ self.presentation_mode = False # displays prev events in a column on the right hand side
+ self.paste_mode = False # currently processing a paste event
+ self.current_match = None # currently tab-selected autocompletion suggestion
+ self.list_win_visible = False # whether the infobox (suggestions, docstring) is visible
+ self.watching_files = False # auto reloading turned on
+ self.special_mode = None # 'reverse_incremental_search' and 'incremental_search'
self.original_modules = sys.modules.keys()
- self.width = None # will both be set by a window resize event
+ self.width = None
self.height = None
self.status_bar.message(banner)
@@ -460,6 +463,12 @@ def process_key_event(self, e):
self.down_one_line()
elif e in ("",):
self.on_control_d()
+ elif e in ("",):
+ self.incremental_search(reverse=True)
+ elif e in ("",):
+ self.incremental_search()
+ elif e in ("", '') and self.special_mode:
+ self.add_to_incremental_search(self, backspace=True)
elif e in self.edit_keys.cut_buffer_edits:
self.readline_kill(e)
elif e in self.edit_keys.simple_edits:
@@ -507,6 +516,21 @@ def process_key_event(self, e):
else:
self.add_normal_character(e)
+ def incremental_search(self, reverse=False):
+ if self.special_mode == None:
+ current_line = ''
+ if reverse:
+ self.special_mode = 'reverse_incremental_search'
+ else:
+ self.special_mode = 'incremental_search'
+ else:
+ self._set_current_line(self.rl_history.back(False, search=True)
+ if reverse else
+ self.rl_history.forward(False, search=True),
+ reset_rl_history=False, clear_special_mode=False)
+ self._set_cursor_offset(len(self.current_line), reset_rl_history=False,
+ clear_special_mode=False)
+
def readline_kill(self, e):
func = self.edit_keys[e]
self.cursor_offset, self.current_line, cut = func(self.cursor_offset, self.current_line)
@@ -659,14 +683,35 @@ def toggle_file_watch(self):
def add_normal_character(self, char):
if len(char) > 1 or is_nop(char):
return
- self.current_line = (self.current_line[:self.cursor_offset] +
- char +
- self.current_line[self.cursor_offset:])
- self.cursor_offset += 1
+ if self.special_mode == 'reverse_incremental_search':
+ self.add_to_incremental_search(char)
+ else:
+ self.current_line = (self.current_line[:self.cursor_offset] +
+ char +
+ self.current_line[self.cursor_offset:])
+ self.cursor_offset += 1
if self.config.cli_trim_prompts and self.current_line.startswith(self.ps1):
self.current_line = self.current_line[4:]
self.cursor_offset = max(0, self.cursor_offset - 4)
+ def add_to_incremental_search(self, char=None, backspace=False):
+ if char is None and not backspace:
+ raise ValueError("must provide a char or set backspace to True")
+ saved_line = self.rl_history.saved_line
+ if backspace:
+ saved_line = saved_line[:-1]
+ else:
+ saved_line += char
+ self.update_completion()
+ self.rl_history.reset()
+ self.rl_history.enter(saved_line)
+ if self.special_mode == 'reverse_incremental_search':
+ self.incremental_search(reverse=True)
+ elif self.special_mode == 'incremental_search':
+ self.incremental_search()
+ else:
+ raise ValueError('add_to_incremental_search should only be called in a special mode')
+
def update_completion(self, tab=False):
"""Update visible docstring and matches, and possibly hide/show completion box"""
#Update autocomplete info; self.matches_iter and self.argspec
@@ -866,6 +911,9 @@ def display_buffer_lines(self):
@property
def display_line_with_prompt(self):
"""colored line with prompt"""
+ if self.special_mode == 'reverse_incremental_search':
+ return func_for_letter(self.config.color_scheme['prompt'])(
+ '(reverse-i-search)`%s\': ' % (self.rl_history.saved_line,)) + self.current_line_formatted
return (func_for_letter(self.config.color_scheme['prompt'])(self.ps1)
if self.done else
func_for_letter(self.config.color_scheme['prompt_more'])(self.ps2)) + self.current_line_formatted
@@ -1085,21 +1133,25 @@ def __repr__(self):
def _get_current_line(self):
return self._current_line
- def _set_current_line(self, line, update_completion=True, reset_rl_history=True):
+ def _set_current_line(self, line, update_completion=True, reset_rl_history=True, clear_special_mode=True):
self._current_line = line
if update_completion:
self.update_completion()
if reset_rl_history:
self.rl_history.reset()
+ if clear_special_mode:
+ self.special_mode = None
current_line = property(_get_current_line, _set_current_line, None,
"The current line")
def _get_cursor_offset(self):
return self._cursor_offset
- def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True):
+ def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True, clear_special_mode=True):
if update_completion:
self.update_completion()
if reset_rl_history:
self.rl_history.reset()
+ if clear_special_mode:
+ self.special_mode = None
self._cursor_offset = offset
self.update_completion()
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 3ac7d5d20..ed79a4cc8 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -38,8 +38,8 @@ def test_startup(self):
def test_enter_text(self):
[self.repl.add_normal_character(c) for c in '1 + 1']
- screen = fsarray([cyan('>>> ') + bold(green('1')+cyan(' ')+
- yellow('+') + cyan(' ') + green('1')), cyan('Welcome to')])
+ screen = fsarray([cyan('>>> ') + bold(blue('1')+cyan(' ')+
+ yellow('+') + cyan(' ') + green('1')), cyan('welcome')])
self.assert_paint(screen, (0, 9))
def test_run_line(self):
From fc7ee66a7a4b40a441179ff8d35c55d71aec7a4e Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 28 Aug 2014 11:10:44 -0700
Subject: [PATCH 0009/1650] don't include current search string as target for
incremental search
---
bpython/curtsiesfrontend/repl.py | 20 ++++++++++++++------
bpython/repl.py | 1 +
2 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index e45438f79..02bff78ab 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -524,12 +524,16 @@ def incremental_search(self, reverse=False):
else:
self.special_mode = 'incremental_search'
else:
- self._set_current_line(self.rl_history.back(False, search=True)
- if reverse else
- self.rl_history.forward(False, search=True),
- reset_rl_history=False, clear_special_mode=False)
- self._set_cursor_offset(len(self.current_line), reset_rl_history=False,
- clear_special_mode=False)
+ if self.rl_history.saved_line:
+ line = (self.rl_history.back(False, search=True)
+ if reverse else
+ self.rl_history.forward(False, search=True))
+ if self.rl_history.is_at_start:
+ line = '' # incremental search's search string isn't the current line
+ self._set_current_line(line,
+ reset_rl_history=False, clear_special_mode=False)
+ self._set_cursor_offset(len(self.current_line),
+ reset_rl_history=False, clear_special_mode=False)
def readline_kill(self, e):
func = self.edit_keys[e]
@@ -695,6 +699,10 @@ def add_normal_character(self, char):
self.cursor_offset = max(0, self.cursor_offset - 4)
def add_to_incremental_search(self, char=None, backspace=False):
+ """Modify the current search term while in incremental search.
+
+ The only operations allowed in incremental search mode are
+ adding characters and backspacing."""
if char is None and not backspace:
raise ValueError("must provide a char or set backspace to True")
saved_line = self.rl_history.saved_line
diff --git a/bpython/repl.py b/bpython/repl.py
index 64b14566a..b46ea77e1 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -139,6 +139,7 @@ def writetb(self, lines):
class History(object):
+ """Stores readline-style history allows access to it"""
def __init__(self, entries=None, duplicates=False):
if entries is None:
From 169536ddf3dd6d29e6e0c5eb734865d86a947f6f Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 28 Aug 2014 13:57:22 -0700
Subject: [PATCH 0010/1650] incremental search and reverse working alright
not very tested yet
---
bpython/curtsiesfrontend/repl.py | 52 ++++++++++++++------------
bpython/repl.py | 63 +++++++++++++++++++-------------
2 files changed, 66 insertions(+), 49 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 02bff78ab..19cac347c 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -307,6 +307,7 @@ def smarter_request_reload(desc):
self.list_win_visible = False # whether the infobox (suggestions, docstring) is visible
self.watching_files = False # auto reloading turned on
self.special_mode = None # 'reverse_incremental_search' and 'incremental_search'
+ self.incremental_search_target = ''
self.original_modules = sys.modules.keys()
@@ -510,30 +511,33 @@ def process_key_event(self, e):
elif e in key_dispatch[self.config.edit_current_block_key]:
self.send_current_block_to_external_editor()
elif e in [""]: #ESC
- pass
+ self.special_mode = None
elif e in [""]:
self.add_normal_character(' ')
else:
self.add_normal_character(e)
- def incremental_search(self, reverse=False):
+ def incremental_search(self, reverse=False, include_current=False):
if self.special_mode == None:
- current_line = ''
- if reverse:
- self.special_mode = 'reverse_incremental_search'
- else:
- self.special_mode = 'incremental_search'
+ self.rl_history.enter(self.current_line)
+ self.incremental_search_target = ''
else:
- if self.rl_history.saved_line:
- line = (self.rl_history.back(False, search=True)
+ if self.incremental_search_target:
+ line = (self.rl_history.back(False, search=True,
+ target=self.incremental_search_target,
+ include_current=include_current)
if reverse else
- self.rl_history.forward(False, search=True))
- if self.rl_history.is_at_start:
- line = '' # incremental search's search string isn't the current line
+ self.rl_history.forward(False, search=True,
+ target=self.incremental_search_target,
+ include_current=include_current))
self._set_current_line(line,
reset_rl_history=False, clear_special_mode=False)
self._set_cursor_offset(len(self.current_line),
reset_rl_history=False, clear_special_mode=False)
+ if reverse:
+ self.special_mode = 'reverse_incremental_search'
+ else:
+ self.special_mode = 'incremental_search'
def readline_kill(self, e):
func = self.edit_keys[e]
@@ -687,7 +691,7 @@ def toggle_file_watch(self):
def add_normal_character(self, char):
if len(char) > 1 or is_nop(char):
return
- if self.special_mode == 'reverse_incremental_search':
+ if self.special_mode:
self.add_to_incremental_search(char)
else:
self.current_line = (self.current_line[:self.cursor_offset] +
@@ -705,18 +709,14 @@ def add_to_incremental_search(self, char=None, backspace=False):
adding characters and backspacing."""
if char is None and not backspace:
raise ValueError("must provide a char or set backspace to True")
- saved_line = self.rl_history.saved_line
if backspace:
- saved_line = saved_line[:-1]
+ self.incremental_search_target = self.incremental_search_target[:-1]
else:
- saved_line += char
- self.update_completion()
- self.rl_history.reset()
- self.rl_history.enter(saved_line)
+ self.incremental_search_target += char
if self.special_mode == 'reverse_incremental_search':
- self.incremental_search(reverse=True)
+ self.incremental_search(reverse=True, include_current=True)
elif self.special_mode == 'incremental_search':
- self.incremental_search()
+ self.incremental_search(include_current=True)
else:
raise ValueError('add_to_incremental_search should only be called in a special mode')
@@ -888,7 +888,10 @@ def current_line_formatted(self):
"""The colored current line (no prompt, not wrapped)"""
if self.config.syntax:
fs = bpythonparse(format(self.tokenize(self.current_line), self.formatter))
- if self.rl_history.saved_line in self.current_line:
+ if self.special_mode:
+ if self.incremental_search_target in self.current_line:
+ fs = fmtfuncs.on_magenta(self.incremental_search_target).join(fs.split(self.incremental_search_target))
+ elif self.rl_history.saved_line and self.rl_history.saved_line in self.current_line:
if self.config.curtsies_right_arrow_completion:
fs = fmtfuncs.on_magenta(self.rl_history.saved_line).join(fs.split(self.rl_history.saved_line))
logger.debug('Display line %r -> %r', self.current_line, fs)
@@ -921,7 +924,10 @@ def display_line_with_prompt(self):
"""colored line with prompt"""
if self.special_mode == 'reverse_incremental_search':
return func_for_letter(self.config.color_scheme['prompt'])(
- '(reverse-i-search)`%s\': ' % (self.rl_history.saved_line,)) + self.current_line_formatted
+ '(reverse-i-search)`%s\': ' % (self.incremental_search_target,)) + self.current_line_formatted
+ elif self.special_mode == 'incremental_search':
+ return func_for_letter(self.config.color_scheme['prompt'])(
+ '(i-search)`%s\': ' % (self.incremental_search_target,)) + self.current_line_formatted
return (func_for_letter(self.config.color_scheme['prompt'])(self.ps1)
if self.done else
func_for_letter(self.config.color_scheme['prompt_more'])(self.ps2)) + self.current_line_formatted
diff --git a/bpython/repl.py b/bpython/repl.py
index b46ea77e1..6dd499f2d 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -139,15 +139,16 @@ def writetb(self, lines):
class History(object):
- """Stores readline-style history allows access to it"""
+ """Stores readline-style history and current place in it"""
def __init__(self, entries=None, duplicates=False):
if entries is None:
self.entries = ['']
else:
self.entries = list(entries)
- self.index = 0
- self.saved_line = ''
+ self.index = 0 # how many lines back in history is currently selected
+ # where 0 is the saved typed line, 1 the prev entered line
+ self.saved_line = '' # what was on the prompt before using history
self.duplicates = duplicates
def append(self, line):
@@ -168,58 +169,68 @@ def first(self):
self.index = len(self.entries)
return self.entries[-self.index]
- def back(self, start=True, search=False):
+ def back(self, start=True, search=False, target=None, include_current=False):
"""Move one step back in the history."""
+ if target is None:
+ target = self.saved_line
if not self.is_at_end:
if search:
- self.index += self.find_partial_match_backward(self.saved_line)
+ self.index += self.find_partial_match_backward(target, include_current)
elif start:
- self.index += self.find_match_backward(self.saved_line)
+ self.index += self.find_match_backward(target, include_current)
else:
self.index += 1
+ return self.entry
+
+ @property
+ def entry(self):
+ """The current entry, which may be the saved line"""
return self.entries[-self.index] if self.index else self.saved_line
- def find_match_backward(self, search_term):
- filtered_list_len = len(self.entries) - self.index
- for idx, val in enumerate(reversed(self.entries[:filtered_list_len])):
+ @property
+ def entries_by_index(self):
+ return list(reversed(self.entries + [self.saved_line]))
+
+ def find_match_backward(self, search_term, include_current=False):
+ for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
if val.startswith(search_term):
- return idx + 1
+ return idx + (0 if include_current else 1)
return 0
- def find_partial_match_backward(self, search_term):
- filtered_list_len = len(self.entries) - self.index
- for idx, val in enumerate(reversed(self.entries[:filtered_list_len])):
+ def find_partial_match_backward(self, search_term, include_current=False):
+ for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
if search_term in val:
- return idx + 1
+ return idx + (0 if include_current else 1)
return 0
- def forward(self, start=True, search=False):
+ def forward(self, start=True, search=False, target=None, include_current=False):
"""Move one step forward in the history."""
+ if target is None:
+ target = self.saved_line
if self.index > 1:
if search:
- self.index -= self.find_partial_match_forward(self.saved_line)
+ self.index -= self.find_partial_match_forward(target, include_current)
elif start:
- self.index -= self.find_match_forward(self.saved_line)
+ self.index -= self.find_match_forward(target, include_current)
else:
self.index -= 1
- return self.entries[-self.index] if self.index else self.saved_line
+ return self.entry
else:
self.index = 0
return self.saved_line
- def find_match_forward(self, search_term):
- filtered_list_len = len(self.entries) - self.index + 1
- for idx, val in enumerate(self.entries[filtered_list_len:]):
+ def find_match_forward(self, search_term, include_current=False):
+ #TODO these are no longer efficient, because we realize the whole list. Does this matter?
+ for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
if val.startswith(search_term):
- return idx + 1
+ return idx + (0 if include_current else 1)
return self.index
- def find_partial_match_forward(self, search_term):
- filtered_list_len = len(self.entries) - self.index + 1
- for idx, val in enumerate(self.entries[filtered_list_len:]):
+ def find_partial_match_forward(self, search_term, include_current=False):
+ for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
if search_term in val:
- return idx + 1
+ return idx + (0 if include_current else 1)
return self.index
From d86d8a8093d56f491b64081c730f6875a72aaf06 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 28 Aug 2014 22:48:18 -0700
Subject: [PATCH 0011/1650] Change hardcoded incremental search keys to meta
(esc+) r and s
---
bpython/curtsiesfrontend/repl.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 19cac347c..4cbdecb40 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -464,9 +464,9 @@ def process_key_event(self, e):
self.down_one_line()
elif e in ("",):
self.on_control_d()
- elif e in ("",):
+ elif e in ("",):
self.incremental_search(reverse=True)
- elif e in ("",):
+ elif e in ("",):
self.incremental_search()
elif e in ("", '') and self.special_mode:
self.add_to_incremental_search(self, backspace=True)
From aea557af6f26db8e1ec5766756c7339d06cac965 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 29 Aug 2014 09:11:04 -0700
Subject: [PATCH 0012/1650] fix #366 and fix #367
adds tests for and fixes external communication, and
turns off welcome banner if help key is disabled
---
bpython/curtsiesfrontend/repl.py | 7 +++++--
bpython/test/test_curtsies_painting.py | 4 ++--
bpython/test/test_curtsies_repl.py | 23 ++++++++++++++++++++++-
3 files changed, 29 insertions(+), 5 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index ca58f042a..d7b2d5783 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -238,7 +238,10 @@ def __init__(self,
if interp is None:
interp = code.InteractiveInterpreter(locals=locals_)
if banner is None:
- banner = _('Welcome to bpython! Press <%s> for help.') % config.help_key
+ if config.help_key:
+ banner = _('Welcome to bpython!') + ' ' + (_('Press <%s> for help.') % config.help_key)
+ else:
+ banner = None
config.autocomplete_mode = autocomplete.SIMPLE # only one implemented currently
if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1:
config.cli_suggestion_width = 1
@@ -1261,7 +1264,7 @@ def show_source(self):
self.pager(source)
def help_text(self):
- return self.version_help_text() + '\n' + self.key_help_text()
+ return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
def version_help_text(self):
return (('bpython-curtsies version %s' % bpython.__version__) + ' ' +
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index ed79a4cc8..3ac7d5d20 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -38,8 +38,8 @@ def test_startup(self):
def test_enter_text(self):
[self.repl.add_normal_character(c) for c in '1 + 1']
- screen = fsarray([cyan('>>> ') + bold(blue('1')+cyan(' ')+
- yellow('+') + cyan(' ') + green('1')), cyan('welcome')])
+ screen = fsarray([cyan('>>> ') + bold(green('1')+cyan(' ')+
+ yellow('+') + cyan(' ') + green('1')), cyan('Welcome to')])
self.assert_paint(screen, (0, 9))
def test_run_line(self):
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index c93467741..d3dfcc4ca 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -1,13 +1,28 @@
import unittest
import sys
+import os
py3 = (sys.version_info[0] == 3)
from bpython.curtsiesfrontend import repl
+from bpython import config
+
+def setup_config(conf):
+ config_struct = config.Struct()
+ config.loadini(config_struct, os.devnull)
+ for key, value in conf.items():
+ if not hasattr(config_struct, key):
+ raise ValueError("%r is not a valid config attribute", (key,))
+ setattr(config_struct, key, value)
+ return config_struct
class TestCurtsiesRepl(unittest.TestCase):
def setUp(self):
- self.repl = repl.Repl()
+ self.config = setup_config({'editor':'true'})
+ self.repl = repl.Repl(config=self.config)
+ os.environ['PAGER'] = 'true'
+ self.repl.width = 50
+ self.repl.height = 20
def test_buffer_finished_will_parse(self):
self.repl.buffer = ['1 + 1']
@@ -23,5 +38,11 @@ def test_buffer_finished_will_parse(self):
self.repl.buffer = ['def foo(x):', ' return 1', '']
self.assertTrue(self.repl.buffer_finished_will_parse(), (True, True))
+ def test_external_communication(self):
+ self.assertEqual(type(self.repl.help_text()), type(b''))
+ self.repl.send_current_block_to_external_editor()
+ self.repl.send_session_to_external_editor()
+
+
if __name__ == '__main__':
unittest.main()
From 0533c80c9c80e03503d7b78bac15e08f1de57f53 Mon Sep 17 00:00:00 2001
From: mlauter
Date: Mon, 18 Aug 2014 16:33:30 -0400
Subject: [PATCH 0013/1650] stop painting standard error red
---
bpython/curtsiesfrontend/repl.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 0bac4f2e3..311c74195 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -787,9 +787,7 @@ def send_to_stderr(self, error):
lines = error.split('\n')
if lines[-1]:
self.current_stdouterr_line += lines[-1]
- self.display_lines.extend([func_for_letter(self.config.color_scheme['error'])(line)
- for line in sum([paint.display_linize(line, self.width, blank_line=True)
- for line in lines[:-1]], [])])
+ self.display_lines.extend(sum([paint.display_linize(line, self.width, blank_line=True) for line in lines[:-1]], []))
def send_to_stdin(self, line):
if line.endswith('\n'):
From 8edeadc7e2dd1de98c7a6e1639800dc0e48061f4 Mon Sep 17 00:00:00 2001
From: mlauter
Date: Mon, 18 Aug 2014 18:27:18 -0400
Subject: [PATCH 0014/1650] add new interactive interpreter subclass with
pretty pygments traceback coloring
---
bpython/curtsiesfrontend/interpreter.py | 168 ++++++++++++++++++++++++
bpython/curtsiesfrontend/repl.py | 3 +-
2 files changed, 170 insertions(+), 1 deletion(-)
create mode 100644 bpython/curtsiesfrontend/interpreter.py
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
new file mode 100644
index 000000000..83586c89a
--- /dev/null
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -0,0 +1,168 @@
+import code
+import traceback
+import sys
+from pygments.style import Style
+from pygments.token import *
+from pygments.formatter import Formatter
+from curtsies.bpythonparse import parse
+from codeop import CommandCompiler, compile_command
+from pygments.lexers import get_lexer_by_name
+from pygments.styles import get_style_by_name
+
+default_colors = {
+ Generic.Error:'R',
+ Keyword:'d',
+ Name:'c',
+ Name.Builtin:'g',
+ Comment:'b',
+ String:'m',
+ Error:'r',
+ Literal:'d',
+ Number:'M',
+ Number.Integer:'d',
+ Operator:'d',
+ Punctuation:'d',
+ Token:'d',
+ Whitespace:'d',
+ Token.Punctuation.Parenthesis:'R',
+ Name.Function:'d',
+ Name.Class:'d',
+ }
+
+
+class BPythonFormatter(Formatter):
+ """This is subclassed from the custom formatter for bpython.
+ Its format() method receives the tokensource
+ and outfile params passed to it from the
+ Pygments highlight() method and slops
+ them into the appropriate format string
+ as defined above, then writes to the outfile
+ object the final formatted string.
+
+ See the Pygments source for more info; it's pretty
+ straightforward."""
+
+ def __init__(self, color_scheme, **options):
+ self.f_strings = {}
+ for k, v in color_scheme.iteritems():
+ self.f_strings[k] = '\x01%s' % (v,)
+ Formatter.__init__(self, **options)
+
+ def format(self, tokensource, outfile):
+ o = ''
+
+ for token, text in tokensource:
+ while token not in self.f_strings:
+ token = token.parent
+ o += "%s\x03%s\x04" % (self.f_strings[token], text)
+ outfile.write(str(parse(o.rstrip())))
+
+class Interp(code.InteractiveInterpreter):
+ def __init__(self, locals=None, outfile=sys.__stderr__):
+ """Constructor.
+
+ The optional 'locals' argument specifies the dictionary in
+ which code will be executed; it defaults to a newly created
+ dictionary with key "__name__" set to "__console__" and key
+ "__doc__" set to None.
+
+ We include an argument for the outfile to pass to the formatter for it to write to.
+
+ """
+ if locals is None:
+ locals = {"__name__": "__console__", "__doc__": None}
+ self.locals = locals
+ self.compile = CommandCompiler()
+ self.outfile = outfile
+
+ def showsyntaxerror(self, filename=None):
+ """Display the syntax error that just occurred.
+
+ This doesn't display a stack trace because there isn't one.
+
+ If a filename is given, it is stuffed in the exception instead
+ of what was there before (because Python's parser always uses
+ "" when reading from a string).
+
+ The output is written by self.write(), below.
+
+ """
+ type, value, sys.last_traceback = sys.exc_info()
+ sys.last_type = type
+ sys.last_value = value
+ if filename and type is SyntaxError:
+ # Work hard to stuff the correct filename in the exception
+ try:
+ msg, (dummy_filename, lineno, offset, line) = value
+ except:
+ # Not the format we expect; leave it alone
+ pass
+ else:
+ # Stuff in the right filename
+ value = SyntaxError(msg, (filename, lineno, offset, line))
+ sys.last_value = value
+ l = traceback.format_exception_only(type, value)
+ tbtext = ''.join(l)
+ lexer = get_lexer_by_name("pytb")
+ traceback_informative_formatter = BPythonFormatter(default_colors)
+ traceback_code_formatter = BPythonFormatter({Token: ('d')})
+ tokens= list(lexer.get_tokens(tbtext))
+ no_format_mode = False
+ cur_line = []
+ for token, text in tokens:
+ if text.endswith('\n'):
+ cur_line.append((token,text))
+ if no_format_mode:
+ traceback_code_formatter.format(cur_line,self.outfile)
+ no_format_mode = False
+ else:
+ traceback_informative_formatter.format(cur_line,self.outfile)
+ cur_line = []
+ elif text == ' ' and cur_line == []:
+ no_format_mode = True
+ cur_line.append((token,text))
+ else:
+ cur_line.append((token,text))
+ assert cur_line == [], cur_line
+
+ def showtraceback(self):
+ """Display the exception that just occurred.
+
+ We remove the first stack item because it is our own code.
+
+
+ """
+ type, value, tb = sys.exc_info()
+ sys.last_type = type
+ sys.last_value = value
+ sys.last_traceback = tb
+ tblist = traceback.extract_tb(tb)
+ del tblist[:1]
+ l = traceback.format_list(tblist)
+ if l:
+ l.insert(0, "Traceback (most recent call last):\n")
+ l[len(l):] = traceback.format_exception_only(type, value)
+ tbtext = ''.join(l)
+ lexer = get_lexer_by_name("pytb", stripall=True)
+ traceback_informative_formatter = BPythonFormatter(default_colors)
+ traceback_code_formatter = BPythonFormatter({Token: ('d')})
+ tokens= list(lexer.get_tokens(tbtext))
+
+ no_format_mode = False
+ cur_line = []
+ for token, text in tokens:
+ if text.endswith('\n'):
+ cur_line.append((token,text))
+ if no_format_mode:
+ traceback_code_formatter.format(cur_line,self.outfile)
+ no_format_mode = False
+ else:
+ traceback_informative_formatter.format(cur_line,self.outfile)
+ cur_line = []
+ elif text == ' ' and cur_line == []:
+ no_format_mode = True
+ cur_line.append((token,text))
+ else:
+ cur_line.append((token,text))
+ assert cur_line == []
+
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 311c74195..3626d05e7 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -17,6 +17,7 @@
from pygments import format
from pygments.lexers import PythonLexer
from pygments.formatters import TerminalFormatter
+from interpreter import Interp
import blessings
@@ -237,7 +238,7 @@ def __init__(self,
# would be unsafe because initial
# state was passed in
if interp is None:
- interp = code.InteractiveInterpreter(locals=locals_)
+ interp = Interp(locals=locals_)
if banner is None:
banner = _('Welcome to bpython! Press <%s> for help.') % config.help_key
config.autocomplete_mode = autocomplete.SIMPLE # only one implemented currently
From b5dac4d9888f358c121aad6960e30b55265783a4 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Mon, 25 Aug 2014 11:59:25 -0400
Subject: [PATCH 0015/1650] Incorporate new interpreter class into the main
repl and use our write traceback method to send to standard error.
---
bpython/curtsiesfrontend/interpreter.py | 16 +++++++++-------
bpython/curtsiesfrontend/repl.py | 2 ++
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index 83586c89a..11cd6aa08 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -29,7 +29,6 @@
Name.Class:'d',
}
-
class BPythonFormatter(Formatter):
"""This is subclassed from the custom formatter for bpython.
Its format() method receives the tokensource
@@ -58,7 +57,7 @@ def format(self, tokensource, outfile):
outfile.write(str(parse(o.rstrip())))
class Interp(code.InteractiveInterpreter):
- def __init__(self, locals=None, outfile=sys.__stderr__):
+ def __init__(self, locals=None):
"""Constructor.
The optional 'locals' argument specifies the dictionary in
@@ -73,7 +72,10 @@ def __init__(self, locals=None, outfile=sys.__stderr__):
locals = {"__name__": "__console__", "__doc__": None}
self.locals = locals
self.compile = CommandCompiler()
- self.outfile = outfile
+
+ # typically changed after being instantiated
+ self.write = lambda stuff: sys.stderr.write(stuff)
+ self.outfile = self
def showsyntaxerror(self, filename=None):
"""Display the syntax error that just occurred.
@@ -113,10 +115,10 @@ def showsyntaxerror(self, filename=None):
if text.endswith('\n'):
cur_line.append((token,text))
if no_format_mode:
- traceback_code_formatter.format(cur_line,self.outfile)
+ traceback_code_formatter.format(cur_line, self.outfile)
no_format_mode = False
else:
- traceback_informative_formatter.format(cur_line,self.outfile)
+ traceback_informative_formatter.format(cur_line, self.outfile)
cur_line = []
elif text == ' ' and cur_line == []:
no_format_mode = True
@@ -154,10 +156,10 @@ def showtraceback(self):
if text.endswith('\n'):
cur_line.append((token,text))
if no_format_mode:
- traceback_code_formatter.format(cur_line,self.outfile)
+ traceback_code_formatter.format(cur_line, self.outfile)
no_format_mode = False
else:
- traceback_informative_formatter.format(cur_line,self.outfile)
+ traceback_informative_formatter.format(cur_line, self.outfile)
cur_line = []
elif text == ' ' and cur_line == []:
no_format_mode = True
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 3626d05e7..f8d9e4b2d 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -239,6 +239,7 @@ def __init__(self,
# state was passed in
if interp is None:
interp = Interp(locals=locals_)
+ interp.writetb = self.send_to_stderr
if banner is None:
banner = _('Welcome to bpython! Press <%s> for help.') % config.help_key
config.autocomplete_mode = autocomplete.SIMPLE # only one implemented currently
@@ -1107,6 +1108,7 @@ def reevaluate(self, insert_into_history=False):
if not self.weak_rewind:
self.interp = self.interp.__class__()
+ self.interp.writetb = self.send_to_stderr
self.coderunner.interp = self.interp
self.buffer = []
From 79d55fa759fae3cf8c0ad09a320702462fbc0b13 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 30 Aug 2014 08:35:31 -0700
Subject: [PATCH 0016/1650] more tests, fix #360
Bit of a cop out, but less tests are being skipped now
---
bpython/autocomplete.py | 11 +++----
bpython/test/test_autocomplete.py | 47 +++++-----------------------
bpython/test/test_manual_readline.py | 10 ++++--
3 files changed, 21 insertions(+), 47 deletions(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index 110c0d762..4071e2c13 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -57,6 +57,9 @@
"oct", "hex", "index", "coerce", "enter", "exit"]]
+def after_last_dot(name):
+ return name.rstrip('.').rsplit('.')[-1]
+
def get_completer(cursor_offset, current_line, locals_, argspec, full_code, mode, complete_magic_methods):
"""Returns a list of matches and a class for what kind of completion is happening
@@ -132,9 +135,7 @@ class ImportCompletion(BaseCompletionType):
def matches(cls, cursor_offset, current_line, **kwargs):
return importcompletion.complete(cursor_offset, current_line)
locate = staticmethod(lineparts.current_word)
- @classmethod
- def format(cls, name):
- return name.rstrip('.').rsplit('.')[-1]
+ format = staticmethod(after_last_dot)
class FilenameCompletion(BaseCompletionType):
shown_before_tab = False
@@ -193,9 +194,7 @@ def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
return matches
locate = staticmethod(lineparts.current_dotted_attribute)
- @classmethod
- def format(cls, name):
- return name.rstrip('.').rsplit('.')[-1]
+ format = staticmethod(after_last_dot)
class DictKeyCompletion(BaseCompletionType):
locate = staticmethod(lineparts.current_dict_key)
diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py
index efedb2501..203297120 100644
--- a/bpython/test/test_autocomplete.py
+++ b/bpython/test/test_autocomplete.py
@@ -9,35 +9,10 @@
def skip(f):
return lambda self: None
-
-# Parts of autocompletion to test:
+#TODO: Parts of autocompletion to test:
# Test that the right matches come back from find_matches (test that priority is correct)
# Test the various complete methods (import, filename) to see if right matches
# Test that MatchesIterator.substitute correctly subs given a match and a completer
-"""
- def test_cw(self):
-
- self.repl.cpos = 2
- self.assertEqual(self.repl.cw(), None)
- self.repl.cpos = 0
-
- self.repl.s = ''
- self.assertEqual(self.repl.cw(), None)
-
- self.repl.s = "this.is.a.test\t"
- self.assertEqual(self.repl.cw(), None)
-
- s = "this.is.a.test"
- self.repl.s = s
- self.assertEqual(self.repl.cw(), s)
-
- s = "\t\tthis.is.a.test"
- self.repl.s = s
- self.assertEqual(self.repl.cw(), s.lstrip())
-
- self.repl.s = "import datetime"
- self.assertEqual(self.repl.cw(), 'datetime')
-"""
class TestSafeEval(unittest.TestCase):
def test_catches_syntax_error(self):
@@ -46,22 +21,16 @@ def test_catches_syntax_error(self):
except:
self.fail('safe_eval raises an error')
-# make some fake files? Dependency inject? mock?
-class TestFilenameCompletion(unittest.TestCase):
- pass
-
-
class TestFormatters(unittest.TestCase):
- @skip('not done yet')
def test_filename(self):
- self.assertEqual(autocomplete.last_part_of_filename('abc'), 'abc')
- self.assertEqual(autocomplete.last_part_of_filename('abc/'), 'abc/')
- self.assertEqual(autocomplete.last_part_of_filename('abc/efg'), 'efg')
- self.assertEqual(autocomplete.last_part_of_filename('abc/efg/'), 'efg/')
- self.assertEqual(autocomplete.last_part_of_filename('/abc'), 'abc')
- self.assertEqual(autocomplete.last_part_of_filename('ab.c/e.f.g/'), 'e.f.g/')
+ last_part_of_filename = autocomplete.FilenameCompletion.format
+ self.assertEqual(last_part_of_filename('abc'), 'abc')
+ self.assertEqual(last_part_of_filename('abc/'), 'abc/')
+ self.assertEqual(last_part_of_filename('abc/efg'), 'efg')
+ self.assertEqual(last_part_of_filename('abc/efg/'), 'efg/')
+ self.assertEqual(last_part_of_filename('/abc'), 'abc')
+ self.assertEqual(last_part_of_filename('ab.c/e.f.g/'), 'e.f.g/')
- @skip('not done yet')
def test_attribute(self):
self.assertEqual(autocomplete.after_last_dot('abc.edf'), 'edf')
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index 96a695310..e577b7331 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -230,10 +230,16 @@ def test_seq(self):
self.assertRaises(KeyError, self.edits.call, 'b')
def test_functions_with_bad_signatures(self):
- pass #TODO
+ f = lambda something: (1, 2)
+ self.assertRaises(TypeError, self.edits.add, 'a', f)
+ g = lambda cursor_offset, line, something, something_else: (1, 2)
+ self.assertRaises(TypeError, self.edits.add, 'a', g)
def test_functions_with_bad_return_values(self):
- pass #TODO
+ f = lambda cursor_offset, line: ('hi',)
+ self.assertRaises(ValueError, self.edits.add, 'a', f)
+ g = lambda cursor_offset, line: ('hi', 1, 2, 3)
+ self.assertRaises(ValueError, self.edits.add, 'b', g)
def test_config(self):
f = lambda cursor_offset, line: ('hi', 2)
From ec8a32ef89a73d1d546160e19432dd45529097ca Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 30 Aug 2014 12:21:05 -0700
Subject: [PATCH 0017/1650] add failing test for #369
---
bpython/curtsies.py | 3 +-
bpython/test/test_args.py | 17 +++++++
bpython/test/test_curtsies_painting.py | 1 -
bpython/test/test_curtsies_repl.py | 66 ++++++++++++++++++++++----
4 files changed, 77 insertions(+), 10 deletions(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 9f80cb247..d4dc29d07 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -45,7 +45,8 @@ def main(args=None, locals_=None, banner=None):
interp = None
paste = None
if exec_args:
- assert options, "don't pass in exec_args without options"
+ if not options:
+ raise ValueError("don't pass in exec_args without options")
exit_value = 0
if options.type:
paste = curtsies.events.PasteEvent()
diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py
index e69de29bb..1d5edc7f7 100644
--- a/bpython/test/test_args.py
+++ b/bpython/test/test_args.py
@@ -0,0 +1,17 @@
+import os
+import sys
+import unittest
+from mock import Mock, MagicMock
+try:
+ from unittest import skip
+except ImportError:
+ def skip(f):
+ return lambda self: None
+
+from bpython import config, repl, cli, autocomplete
+
+class TestFutureImports(unittest.TestCase):
+
+ def test_interactive(self):
+ pass
+
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 3ac7d5d20..b27693a93 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -18,7 +18,6 @@ def setup_config():
class TestCurtsiesPainting(FormatStringTest):
def setUp(self):
- self.refresh_requests = []
self.repl = Repl(config=setup_config())
self.repl.rl_history = History() # clear history
self.repl.height, self.repl.width = (5, 10)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index d3dfcc4ca..b0c8a4c8d 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -1,10 +1,22 @@
-import unittest
-import sys
+import code
import os
+import sys
+import tempfile
+from contextlib import contextmanager
+from StringIO import StringIO
+
+import unittest
+try:
+ from unittest import skip
+except ImportError:
+ def skip(f):
+ return lambda self: None
+
py3 = (sys.version_info[0] == 3)
-from bpython.curtsiesfrontend import repl
+from bpython.curtsiesfrontend import repl as curtsiesrepl
from bpython import config
+from bpython import args
def setup_config(conf):
config_struct = config.Struct()
@@ -18,11 +30,7 @@ def setup_config(conf):
class TestCurtsiesRepl(unittest.TestCase):
def setUp(self):
- self.config = setup_config({'editor':'true'})
- self.repl = repl.Repl(config=self.config)
- os.environ['PAGER'] = 'true'
- self.repl.width = 50
- self.repl.height = 20
+ self.repl = create_repl()
def test_buffer_finished_will_parse(self):
self.repl.buffer = ['1 + 1']
@@ -43,6 +51,48 @@ def test_external_communication(self):
self.repl.send_current_block_to_external_editor()
self.repl.send_session_to_external_editor()
+@contextmanager # from http://stackoverflow.com/a/17981937/398212 - thanks @rkennedy
+def captured_output():
+ new_out, new_err = StringIO(), StringIO()
+ old_out, old_err = sys.stdout, sys.stderr
+ try:
+ sys.stdout, sys.stderr = new_out, new_err
+ yield sys.stdout, sys.stderr
+ finally:
+ sys.stdout, sys.stderr = old_out, old_err
+
+def create_repl(**kwargs):
+ config = setup_config({'editor':'true'})
+ repl = curtsiesrepl.Repl(config=config, **kwargs)
+ os.environ['PAGER'] = 'true'
+ repl.width = 50
+ repl.height = 20
+ return repl
+
+class TestFutureImports(unittest.TestCase):
+
+ def test_repl(self):
+ repl = create_repl()
+ with captured_output() as (out, err):
+ repl.push('from __future__ import division')
+ repl.push('1 / 2')
+ self.assertEqual(out.getvalue(), '0.5\n')
+
+ @skip('Failing - this is issue #369')
+ def test_interactive(self):
+ interp = code.InteractiveInterpreter(locals={})
+ with captured_output() as (out, err):
+ with tempfile.NamedTemporaryFile(suffix='.py') as f:
+ f.write('from __future__ import division\n')
+ f.write('print 1/2\n')
+ f.flush()
+ args.exec_code(interp, [f.name])
+
+ repl = create_repl(interp=interp)
+ repl.push('1 / 2')
+
+ self.assertEqual(out.getvalue(), '0.5\n0.5\n')
+
if __name__ == '__main__':
unittest.main()
From 84ac7a249e96955c48c5863c7b529e203360de33 Mon Sep 17 00:00:00 2001
From: Alice Chen
Date: Wed, 3 Sep 2014 12:13:29 -0400
Subject: [PATCH 0018/1650] Fixed 341 right arrow suggestion color
---
bpython/config.py | 1 +
bpython/curtsiesfrontend/repl.py | 3 ++-
doc/sphinx/source/themes.rst | 2 ++
light.theme | 1 +
sample.theme | 1 +
5 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/bpython/config.py b/bpython/config.py
index df27894b3..9aa01cf86 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -178,6 +178,7 @@ def loadini(struct, configfile):
'paren': 'R',
'prompt': 'c',
'prompt_more': 'g',
+ 'right_arrow_suggestion': 'K',
}
if color_scheme_name == 'default':
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index d7b2d5783..785252b8f 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -948,7 +948,8 @@ def current_cursor_line_without_suggestion(self):
@property
def current_cursor_line(self):
if self.config.curtsies_right_arrow_completion:
- return self.current_cursor_line_without_suggestion + fmtfuncs.bold(fmtfuncs.dark((self.current_suggestion)))
+ return (self.current_cursor_line_without_suggestion +
+ func_for_letter(self.config.color_scheme['right_arrow_suggestion'])(self.current_suggestion))
else:
return self.current_cursor_line_without_suggestion
diff --git a/doc/sphinx/source/themes.rst b/doc/sphinx/source/themes.rst
index 3dab790eb..c28875be4 100644
--- a/doc/sphinx/source/themes.rst
+++ b/doc/sphinx/source/themes.rst
@@ -38,6 +38,7 @@ Available Switches
* main
* prompt
* prompt_more
+* right_arrow_suggestion
Default Theme
-------------
@@ -74,6 +75,7 @@ The default theme included in bpython is as follows:
main = c
prompt = c
prompt_more = g
+ right_arrow_suggestion = K
.. :: Footnotes
diff --git a/light.theme b/light.theme
index 144b48ee1..448d9dc6a 100644
--- a/light.theme
+++ b/light.theme
@@ -26,3 +26,4 @@ output = b
main = b
prompt = r
prompt_more = g
+right_arrow_suggestion = K
diff --git a/sample.theme b/sample.theme
index ee932be78..af1e3cab7 100644
--- a/sample.theme
+++ b/sample.theme
@@ -27,3 +27,4 @@ output = w
main = c
prompt = c
prompt_more = g
+right_arrow_suggestion = K
From b40ce7d5563ca867e7d13cf358c0b2cc3f516259 Mon Sep 17 00:00:00 2001
From: mlauter
Date: Thu, 4 Sep 2014 11:39:20 -0400
Subject: [PATCH 0019/1650] stop str-ing in Interp.write, send FmtStr to stderr
instead
---
bpython/curtsiesfrontend/interpreter.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index 11cd6aa08..5459692b2 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -36,7 +36,7 @@ class BPythonFormatter(Formatter):
Pygments highlight() method and slops
them into the appropriate format string
as defined above, then writes to the outfile
- object the final formatted string.
+ object the final formatted string. This does not write real strings. It writes format string (FmtStr) objects.
See the Pygments source for more info; it's pretty
straightforward."""
@@ -54,7 +54,7 @@ def format(self, tokensource, outfile):
while token not in self.f_strings:
token = token.parent
o += "%s\x03%s\x04" % (self.f_strings[token], text)
- outfile.write(str(parse(o.rstrip())))
+ outfile.write(parse(o.rstrip()))
class Interp(code.InteractiveInterpreter):
def __init__(self, locals=None):
@@ -146,6 +146,8 @@ def showtraceback(self):
l[len(l):] = traceback.format_exception_only(type, value)
tbtext = ''.join(l)
lexer = get_lexer_by_name("pytb", stripall=True)
+
+
traceback_informative_formatter = BPythonFormatter(default_colors)
traceback_code_formatter = BPythonFormatter({Token: ('d')})
tokens= list(lexer.get_tokens(tbtext))
From 8aca9a5de7170602ae922c7bc78c49ab09464345 Mon Sep 17 00:00:00 2001
From: mlauter
Date: Thu, 4 Sep 2014 12:45:52 -0400
Subject: [PATCH 0020/1650] factor out redundant code from showtraceback and
showsyntaxerror
---
bpython/curtsiesfrontend/interpreter.py | 27 +++++--------------------
1 file changed, 5 insertions(+), 22 deletions(-)
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index 5459692b2..7013b436b 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -106,26 +106,7 @@ def showsyntaxerror(self, filename=None):
l = traceback.format_exception_only(type, value)
tbtext = ''.join(l)
lexer = get_lexer_by_name("pytb")
- traceback_informative_formatter = BPythonFormatter(default_colors)
- traceback_code_formatter = BPythonFormatter({Token: ('d')})
- tokens= list(lexer.get_tokens(tbtext))
- no_format_mode = False
- cur_line = []
- for token, text in tokens:
- if text.endswith('\n'):
- cur_line.append((token,text))
- if no_format_mode:
- traceback_code_formatter.format(cur_line, self.outfile)
- no_format_mode = False
- else:
- traceback_informative_formatter.format(cur_line, self.outfile)
- cur_line = []
- elif text == ' ' and cur_line == []:
- no_format_mode = True
- cur_line.append((token,text))
- else:
- cur_line.append((token,text))
- assert cur_line == [], cur_line
+ self.format(tbtext,lexer)
def showtraceback(self):
"""Display the exception that just occurred.
@@ -147,7 +128,10 @@ def showtraceback(self):
tbtext = ''.join(l)
lexer = get_lexer_by_name("pytb", stripall=True)
+ self.format(tbtext,lexer)
+
+ def format(self, tbtext, lexer):
traceback_informative_formatter = BPythonFormatter(default_colors)
traceback_code_formatter = BPythonFormatter({Token: ('d')})
tokens= list(lexer.get_tokens(tbtext))
@@ -168,5 +152,4 @@ def showtraceback(self):
cur_line.append((token,text))
else:
cur_line.append((token,text))
- assert cur_line == []
-
+ assert cur_line == [], cur_line
From 55ce0e753ae5d5ad73f7319e7fd7cb933e1649c4 Mon Sep 17 00:00:00 2001
From: mlauter
Date: Thu, 4 Sep 2014 12:46:16 -0400
Subject: [PATCH 0021/1650] add test for interpreter
---
bpython/test/test_interpreter.py | 42 ++++++++++++++++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 bpython/test/test_interpreter.py
diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py
new file mode 100644
index 000000000..bb2cc29cb
--- /dev/null
+++ b/bpython/test/test_interpreter.py
@@ -0,0 +1,42 @@
+import unittest
+
+from bpython.curtsiesfrontend import interpreter
+from curtsies.fmtfuncs import *
+
+class TestInterpreter(unittest.TestCase):
+ def test_syntaxerror(self):
+ i = interpreter.Interp()
+ a = []
+
+ def append_to_a(message):
+ a.append(message)
+
+ i.write = append_to_a
+ i.runsource('1.1.1.1')
+
+ expected = ''+u''+u' File '+green(u'""')+u', line '+bold(magenta(u'1'))+u'\n'+u' '+u'1.1'+u'.'+u'1.1'+u'\n'+u' '+u' '+u'^'+u'\n'+bold(red(u'SyntaxError'))+u': '+cyan(u'invalid syntax')+u'\n'
+
+ self.assertEquals(str(plain('').join(a)), str(expected))
+ self.assertEquals(plain('').join(a), expected)
+
+ def test_traceback(self):
+ i = interpreter.Interp()
+ a = []
+
+ def append_to_a(message):
+ a.append(message)
+
+ i.write = append_to_a
+
+ def f():
+ return 1/0
+
+ def g():
+ return f()
+
+ i.runsource('g()')
+
+ expected = u'Traceback (most recent call last):\n'+''+u' File '+green(u'""')+u', line '+bold (magenta(u'1'))+u', in '+cyan(u'')+u'\n'+''+bold(red(u'NameError'))+u': '+cyan(u"name 'g' is not defined")+u'\n'
+
+ self.assertEquals(str(plain('').join(a)), str(expected))
+ self.assertEquals(plain('').join(a), expected)
From 51126fa8f72b30ee0262477ae82f609fa86ab114 Mon Sep 17 00:00:00 2001
From: DevOnLinux
Date: Thu, 11 Sep 2014 13:05:53 -0400
Subject: [PATCH 0022/1650] Added docs for new keys. fix #338
---
doc/sphinx/source/configuration-options.rst | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index 1aa79b4af..386330896 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -175,6 +175,23 @@ Reruns entire session, reloading all modules by clearing the sys.modules cache.
.. versionadded:: 0.14
+help
+^^^^
+Default: F1
+
+Brings up sincerely cheerful description of bpython features and current key bindings.
+
+.. versionadded:: 0.14
+
+toggle_file_watch
+^^^^^^^^^^^^^^^^^
+Default: F5
+
+Toggles file watching behaviour; re-runs entire bpython session whenever an imported
+module is modified.
+
+.. versionadded:: 0.14
+
save
^^^^
Default: C-s
From b80b4009c3845c127de7e16a5f8f784c8e2a62ee Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 11 Sep 2014 16:24:27 -0400
Subject: [PATCH 0023/1650] remove trailing whitespace
---
doc/sphinx/source/configuration-options.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index 386330896..b4810de9e 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -187,7 +187,7 @@ toggle_file_watch
^^^^^^^^^^^^^^^^^
Default: F5
-Toggles file watching behaviour; re-runs entire bpython session whenever an imported
+Toggles file watching behaviour; re-runs entire bpython session whenever an imported
module is modified.
.. versionadded:: 0.14
From d0f0394c62c25d4bdb6daf9cd8c2dc6b891f830d Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 11 Sep 2014 20:16:51 -0400
Subject: [PATCH 0024/1650] fix control-c? (Seems to work, could use review)
---
bpython/cli.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/bpython/cli.py b/bpython/cli.py
index ebaebe809..78ee771d9 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -999,6 +999,9 @@ def p_key(self, key):
elif key == '\x18':
return self.send_current_line_to_editor()
+ elif key == '\x03':
+ raise KeyboardInterrupt()
+
elif key[0:3] == 'PAD' and not key in ('PAD0', 'PADSTOP'):
pad_keys = {
'PADMINUS': '-',
From 2fd97980f15531b8fa33496e620d560b8fffe627 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Mon, 8 Sep 2014 16:49:16 -0400
Subject: [PATCH 0025/1650] take back line working for bpythno-curtsies
---
bpython/curtsiesfrontend/repl.py | 7 +++++++
bpython/repl.py | 6 ++++--
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 419c9fce1..e4e854da2 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1193,6 +1193,13 @@ def reprint_line(self, lineno, tokens):
logger.debug("calling reprint line with %r %r", lineno, tokens)
if self.config.syntax:
self.display_buffer[lineno] = bpythonparse(format(tokens, self.formatter))
+
+ def take_back_buffer_line(self):
+ self.display_buffer.pop()
+ self.buffer.pop()
+ self.cursor_offset = 0
+ self.current_line = ''
+
def reevaluate(self, insert_into_history=False):
"""bpython.Repl.undo calls this"""
if self.watcher: self.watcher.reset()
diff --git a/bpython/repl.py b/bpython/repl.py
index 221e7a176..76271a38d 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -902,8 +902,10 @@ def undo(self, n=1):
entries = list(self.rl_history.entries)
self.history = self.history[:-n]
-
- self.reevaluate()
+ if n == 1 and not self.done:
+ self.take_back_buffer_line()
+ else:
+ self.reevaluate()
self.rl_history.entries = entries
From cc0ae82cefe0ee393c65763a8c2bd25c4fd0c71e Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Mon, 8 Sep 2014 17:07:21 -0400
Subject: [PATCH 0026/1650] indentation works for new rewind
---
bpython/curtsiesfrontend/repl.py | 32 ++++++++++++++++++++++----------
1 file changed, 22 insertions(+), 10 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index e4e854da2..b3d78906e 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -735,6 +735,18 @@ def update_completion(self, tab=False):
self.current_match = None
self.list_win_visible = BpythonRepl.complete(self, tab)
+ def predicted_indent(self, line):
+ logger.debug('line is %r', line)
+ indent = len(re.match(r'[ ]*', line).group())
+ if line.endswith(':'):
+ indent = max(0, indent + self.config.tab_length)
+ elif line and line.count(' ') == len(line):
+ indent = max(0, indent - self.config.tab_length)
+ elif line and ':' not in line and line.strip().startswith(('return', 'pass', 'raise', 'yield')):
+ indent = max(0, indent - self.config.tab_length)
+ logger.debug('indent we found was %s', indent)
+ return indent
+
def push(self, line, insert_into_history=True):
"""Push a line of code onto the buffer, start running the buffer
@@ -743,14 +755,7 @@ def push(self, line, insert_into_history=True):
if self.paste_mode:
self.saved_indent = 0
else:
- indent = len(re.match(r'[ ]*', line).group())
- if line.endswith(':'):
- indent = max(0, indent + self.config.tab_length)
- elif line and line.count(' ') == len(line):
- indent = max(0, indent - self.config.tab_length)
- elif line and ':' not in line and line.strip().startswith(('return', 'pass', 'raise', 'yield')):
- indent = max(0, indent - self.config.tab_length)
- self.saved_indent = indent
+ self.saved_indent = self.predicted_indent(line)
#current line not added to display buffer if quitting #TODO I don't understand this comment
if self.config.syntax:
@@ -1197,8 +1202,15 @@ def reprint_line(self, lineno, tokens):
def take_back_buffer_line(self):
self.display_buffer.pop()
self.buffer.pop()
- self.cursor_offset = 0
- self.current_line = ''
+
+ if not self.buffer:
+ self.current_line = ''
+ self.cursor_offset = 0
+ else:
+ line = self.buffer[-1]
+ indent = self.predicted_indent(line)
+ self.current_line = indent * ' '
+ self.cursor_offset = len(self.current_line)
def reevaluate(self, insert_into_history=False):
"""bpython.Repl.undo calls this"""
From a0f2e2acd03e55f3a95881570b7f96b2597cef76 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Mon, 8 Sep 2014 17:14:41 -0400
Subject: [PATCH 0027/1650] Added tests
---
bpython/test/test_curtsies_repl.py | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index b0c8a4c8d..9f1b3ff21 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -93,6 +93,22 @@ def test_interactive(self):
self.assertEqual(out.getvalue(), '0.5\n0.5\n')
+class TestPredictedIndent(unittest.TestCase):
+ def setUp(self):
+ self.repl = create_repl()
+
+ def test_simple(self):
+ self.assertEqual(self.repl.predicted_indent(''), 0)
+ self.assertEqual(self.repl.predicted_indent('class Foo:'), 4)
+ self.assertEqual(self.repl.predicted_indent('class Foo: pass'), 0)
+ self.assertEqual(self.repl.predicted_indent('def asdf():'), 4)
+ self.assertEqual(self.repl.predicted_indent('def asdf(): return 7'), 0)
+
+ @skip
+ def test_complex(self):
+ self.assertEqual(self.repl.predicted_indent('[a,'), 1)
+ self.assertEqual(self.repl.predicted_indent('reduce(asdfasdf,'), 7)
+
if __name__ == '__main__':
unittest.main()
From f9fa7e8d60edb17528ae4afcf3dd16f8794307ea Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Mon, 8 Sep 2014 21:36:50 -0400
Subject: [PATCH 0028/1650] make other frontends work
---
bpython/curtsiesfrontend/repl.py | 1 +
bpython/repl.py | 3 ++-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index b3d78906e..4a25ead87 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -736,6 +736,7 @@ def update_completion(self, tab=False):
self.list_win_visible = BpythonRepl.complete(self, tab)
def predicted_indent(self, line):
+ #TODO get rid of this! It's repeated code! Combine with Repl.
logger.debug('line is %r', line)
indent = len(re.match(r'[ ]*', line).group())
if line.endswith(':'):
diff --git a/bpython/repl.py b/bpython/repl.py
index 76271a38d..c1a654b93 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -902,7 +902,8 @@ def undo(self, n=1):
entries = list(self.rl_history.entries)
self.history = self.history[:-n]
- if n == 1 and not self.done:
+ if (n == 1 and self.buffer and
+ hasattr(self, 'take_back_buffer_line')):
self.take_back_buffer_line()
else:
self.reevaluate()
From a66607159d206f376ce4722805918a7011527071 Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Thu, 11 Sep 2014 17:17:37 -0400
Subject: [PATCH 0029/1650] pressing prints the last word of the last
command to the command line Added test for get last word
---
bpython/curtsiesfrontend/repl.py | 25 ++++++++++++++++++++++++-
bpython/test/test_curtsies_repl.py | 9 +++++++++
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 419c9fce1..fd22d9802 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -46,6 +46,7 @@
#TODO other autocomplete modes (also fix in other bpython implementations)
+
from curtsies.configfile_keynames import keymap as key_dispatch
logger = logging.getLogger(__name__)
@@ -87,6 +88,7 @@ def __init__(self, coderunner, repl, configured_edit_keys=None):
def process_event(self, e):
assert self.has_focus
+
logger.debug('fake input processing event %r', e)
if isinstance(e, events.PasteEvent):
for ee in e.events:
@@ -100,6 +102,9 @@ def process_event(self, e):
self.current_line = ''
self.cursor_offset = 0
self.repl.run_code_and_maybe_finish()
+ elif e in ("",):
+ self.get_last_word()
+
elif e in [""]:
pass
elif e in ['']:
@@ -469,6 +474,8 @@ def process_key_event(self, e):
self.down_one_line()
elif e in ("",):
self.on_control_d()
+ elif e in ("",):
+ self.get_last_word()
elif e in ("",):
self.incremental_search(reverse=True)
elif e in ("",):
@@ -524,6 +531,21 @@ def process_key_event(self, e):
else:
self.add_normal_character(e)
+ def get_last_word(self):
+
+ def last_word(line):
+ if not line:
+ return ''
+ return line.split().pop()
+
+ previous_word = last_word(self.rl_history.entry)
+ word = last_word(self.rl_history.back())
+ line=self.current_line
+ self._set_current_line(line[:len(line)-len(previous_word)]+word, reset_rl_history=False)
+
+ self._set_cursor_offset(self.cursor_offset-len(previous_word)+len(word), reset_rl_history=False)
+
+
def incremental_search(self, reverse=False, include_current=False):
if self.special_mode == None:
self.rl_history.enter(self.current_line)
@@ -808,7 +830,8 @@ def run_code_and_maybe_finish(self, for_code=None):
if err:
indent = 0
- #TODO This should be printed ABOVE the error that just happened instead
+
+ #TODO This should be printed ABOVE the error that just happened instead
# or maybe just thrown away and not shown
if self.current_stdouterr_line:
self.display_lines.extend(paint.display_linize(self.current_stdouterr_line, self.width))
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index b0c8a4c8d..21a2b3f57 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -51,6 +51,15 @@ def test_external_communication(self):
self.repl.send_current_block_to_external_editor()
self.repl.send_session_to_external_editor()
+ def test_get_last_word(self):
+ self.repl.rl_history.entries=['1','2 3','4 5 6']
+ self.repl._set_current_line('abcde')
+ self.repl.get_last_word()
+ self.assertEqual(self.repl.current_line,'abcde6')
+ self.repl.get_last_word()
+ self.assertEqual(self.repl.current_line,'abcde3')
+
+
@contextmanager # from http://stackoverflow.com/a/17981937/398212 - thanks @rkennedy
def captured_output():
new_out, new_err = StringIO(), StringIO()
From 9a6cadb301b927ebb6b6fe352295a781e845e099 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 16 Sep 2014 11:44:40 -0400
Subject: [PATCH 0030/1650] add test documenting bash's meta-. history behavior
---
bpython/test/test_curtsies_repl.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index 246fce18a..bfaafba45 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -59,6 +59,18 @@ def test_get_last_word(self):
self.repl.get_last_word()
self.assertEqual(self.repl.current_line,'abcde3')
+ @skip # this is the behavior of bash - not currently implemented
+ def test_get_last_word_with_prev_line
+ self.repl.rl_history.entries=['1','2 3','4 5 6']
+ self.repl._set_current_line('abcde')
+ self.repl.up_one_line()
+ self.assertEqual(self.repl.current_line,'4 5 6')
+ self.repl.get_last_word()
+ self.assertEqual(self.repl.current_line,'4 5 63')
+ self.repl.get_last_word()
+ self.assertEqual(self.repl.current_line,'4 5 64')
+ self.repl.up_one_line()
+ self.assertEqual(self.repl.current_line,'2 3')
@contextmanager # from http://stackoverflow.com/a/17981937/398212 - thanks @rkennedy
def captured_output():
From 9def303254b429db7bc575821b0f7b18e7e2fb76 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 16 Sep 2014 12:11:13 -0400
Subject: [PATCH 0031/1650] fix broken test that broke the build
---
bpython/test/test_curtsies_repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index bfaafba45..ab496be2a 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -60,7 +60,7 @@ def test_get_last_word(self):
self.assertEqual(self.repl.current_line,'abcde3')
@skip # this is the behavior of bash - not currently implemented
- def test_get_last_word_with_prev_line
+ def test_get_last_word_with_prev_line(self):
self.repl.rl_history.entries=['1','2 3','4 5 6']
self.repl._set_current_line('abcde')
self.repl.up_one_line()
From 882c1f371557d1d02a438d5ab3d5af2707cf05b9 Mon Sep 17 00:00:00 2001
From: Tony Wang
Date: Thu, 18 Sep 2014 22:31:04 +0800
Subject: [PATCH 0032/1650] consistent return for delete_word_from_cursor_back
bpython would crash when option-backspace on empty line
---
bpython/curtsiesfrontend/manual_readline.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py
index dd51d590e..4be8bb961 100644
--- a/bpython/curtsiesfrontend/manual_readline.py
+++ b/bpython/curtsiesfrontend/manual_readline.py
@@ -268,7 +268,7 @@ def titlecase_next_word(cursor_offset, line):
def delete_word_from_cursor_back(cursor_offset, line):
"""Whatever my option-delete does in bash on my mac"""
if not line:
- return cursor_offset, line
+ return cursor_offset, line, ''
starts = [m.start() for m in list(re.finditer(r'\b\w', line)) if m.start() < cursor_offset]
if starts:
return starts[-1], line[:starts[-1]] + line[cursor_offset:], line[starts[-1]:cursor_offset]
From ce19978aec2288237b873717867f92e31d443fda Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 18 Sep 2014 13:10:22 -0400
Subject: [PATCH 0033/1650] Add tests for nop case of some readline commands
---
bpython/test/test_manual_readline.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index e577b7331..9cd6c286b 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -144,6 +144,7 @@ def test_delete_from_cursor_forward(self):
expected = (pos, "everything after ")
result = delete_from_cursor_forward(line.find("this"), line)[:-1]
self.assertEquals(expected, result)
+ self.assertEquals(delete_from_cursor_forward(0, ''), (0, '', ''))
def test_delete_rest_of_word(self):
self.try_stages_kill(['z|s;df asdf d s;a;a',
@@ -153,6 +154,7 @@ def test_delete_rest_of_word(self):
'z| s;a;a',
'z|;a;a',
'z|;a',
+ 'z|',
'z|'], delete_rest_of_word)
def test_delete_word_to_cursor(self):
@@ -163,7 +165,7 @@ def test_delete_word_to_cursor(self):
' a;d |a',
' |a',
'|a',
- ], delete_word_to_cursor)
+ '|a'], delete_word_to_cursor)
def test_yank_prev_killed_text(self):
pass
@@ -212,7 +214,8 @@ def test_delete_word_from_cursor_back(self):
"asd;fljk asd;|",
"asd;fljk |",
"asd;|",
- "|"], delete_word_from_cursor_back)
+ "|",
+ "|"], delete_word_from_cursor_back)
class TestEdits(unittest.TestCase):
From 4795738a16ad330a67156df9778ae14ceeb7d00b Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 19 Sep 2014 16:26:05 +0200
Subject: [PATCH 0034/1650] Use new JSON interface of bpaste.net and remove the
old XML-RPC interface
This change adds a dependency on requests.
Signed-off-by: Sebastian Ramacher
---
bpython/config.py | 8 +++-
bpython/repl.py | 45 ++++++++++++---------
doc/sphinx/source/configuration-options.rst | 20 ++++++---
setup.py | 3 +-
4 files changed, 50 insertions(+), 26 deletions(-)
diff --git a/bpython/config.py b/bpython/config.py
index 9aa01cf86..5949b78d9 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -53,9 +53,11 @@ def loadini(struct, configfile):
'tab_length': 4,
'pastebin_confirm': True,
'pastebin_private': False,
- 'pastebin_url': 'http://bpaste.net/xmlrpc/',
+ 'pastebin_url': 'https://bpaste.net/json/new',
'pastebin_private': True,
- 'pastebin_show_url': 'http://bpaste.net/show/$paste_id/',
+ 'pastebin_show_url': 'https://bpaste.net/show/$paste_id',
+ 'pastebin_removal_url': 'https://bpaste.net/remove/$removal_id',
+ 'pastebin_expiry': '1week',
'pastebin_helper': '',
'save_append_py': False,
'editor': os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
@@ -144,6 +146,8 @@ def loadini(struct, configfile):
struct.pastebin_url = config.get('general', 'pastebin_url')
struct.pastebin_private = config.get('general', 'pastebin_private')
struct.pastebin_show_url = config.get('general', 'pastebin_show_url')
+ struct.pastebin_removal_url = config.get('general', 'pastebin_removal_url')
+ struct.pastebin_expiry = config.get('general', 'pastebin_expiry')
struct.pastebin_helper = config.get('general', 'pastebin_helper')
struct.cli_suggestion_width = config.getfloat('cli',
diff --git a/bpython/repl.py b/bpython/repl.py
index c1a654b93..883209509 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -29,6 +29,7 @@
import logging
import os
import pydoc
+import requests
import shlex
import subprocess
import sys
@@ -41,8 +42,7 @@
from socket import error as SocketError
from string import Template
from urllib import quote as urlquote
-from urlparse import urlparse
-from xmlrpclib import ServerProxy, Error as XMLRPCError
+from urlparse import urlparse, urljoin
from pygments.token import Token
@@ -783,32 +783,41 @@ def do_pastebin(self, s):
if self.config.pastebin_helper:
return self.do_pastebin_helper(s)
else:
- return self.do_pastebin_xmlrpc(s)
+ return self.do_pastebin_json(s)
- def do_pastebin_xmlrpc(self, s):
- """Upload to pastebin via XML-RPC."""
- try:
- pasteservice = ServerProxy(self.config.pastebin_url)
- except IOError, e:
- self.interact.notify(_("Pastebin error for URL '%s': %s") %
- (self.config.pastebin_url, str(e)))
- return
+ def do_pastebin_json(self, s):
+ """Upload to pastebin via json interface."""
+
+ url = urljoin(self.config.pastebin_url, '/json/new')
+ payload = {
+ 'code': s,
+ 'lexer': 'pycon',
+ 'expiry': self.config.pastebin_expiry
+ }
self.interact.notify(_('Posting data to pastebin...'))
try:
- paste_id = pasteservice.pastes.newPaste('pycon', s, '', '', '',
- self.config.pastebin_private)
- except (SocketError, XMLRPCError), e:
- self.interact.notify(_('Upload failed: %s') % (str(e), ) )
- return
+ response = requests.post(url, data=payload, verify=True)
+ response.raise_for_status()
+ except requests.exceptions.RequestException as exc:
+ self.interact.notify(_('Upload failed: %s') % (str(exc), ))
+ return
self.prev_pastebin_content = s
+ data = response.json()
paste_url_template = Template(self.config.pastebin_show_url)
- paste_id = urlquote(paste_id)
+ paste_id = urlquote(data['paste_id'])
paste_url = paste_url_template.safe_substitute(paste_id=paste_id)
+
+ removal_url_template = Template(self.config.pastebin_removal_url)
+ removal_id = urlquote(data['removal_id'])
+ removal_url = removal_url_template.safe_substitute(removal_id=removal_id)
+
self.prev_pastebin_url = paste_url
- self.interact.notify(_('Pastebin URL: %s') % (paste_url, ), 10)
+ self.interact.notify(_('Pastebin URL: %s - Removal URL: %s') %
+ (paste_url, removal_url))
+
return paste_url
def do_pastebin_helper(self, s):
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index b4810de9e..6f2a06c82 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -44,8 +44,8 @@ Soft tab size (default 4, see pep-8)
pastebin_url
^^^^^^^^^^^^
The pastebin url to post to (without a trailing slash). This pastebin has to be
-a pastebin which uses LodgeIt. Examples are: http://paste.pocoo.org/xmlrpc/ and
-http://bpaste.net/xmlrpc/ (default: http://bpaste.net/xmlrpc/)
+a pastebin which uses provides a similar interface to ``bpaste.net``'s JSON
+interface. (default: https://bpaste.net/json/new)
pastebin_private
^^^^^^^^^^^^^^^^
@@ -57,9 +57,19 @@ Default: True).
pastebin_show_url
^^^^^^^^^^^^^^^^^
The url under which the new paste can be reached. ``$paste_id`` will be replaced
-by the ID of the new paste. Examples are: http://bpaste.net/show/$paste_id/ and
-http://paste.pocoo.org/show/$paste_id/ (default:
-http://bpaste.net/show/$paste_id/)
+by the ID of the new paste. (default: https://bpaste.net/show/$paste_id/)
+
+pastebin_removal_url
+^^^^^^^^^^^^^^^^^^^^
+The url under which a paste can be removed. ``$removal_id`` will be replaced
+by the removal ID of the paste. (default: https://bpaste.net/remova/$removal_id/)
+
+.. versionadded:: 0.14
+
+pastebin_expiry
+^^^^^^^^^^^^^^^
+Time duration after which a paste should expire. Valid values are ``1day``,
+``1week`` and ``1month``. (default: ``1week``)
pastebin_helper
^^^^^^^^^^^^^^^
diff --git a/setup.py b/setup.py
index 8de734e8c..83fa8ab7f 100755
--- a/setup.py
+++ b/setup.py
@@ -177,7 +177,8 @@ def initialize_options(self):
long_description = """bpython is a fancy interface to the Python
interpreter for Unix-like operating systems.""",
install_requires = [
- 'pygments'
+ 'pygments',
+ 'requests'
],
extras_require = extras_require,
tests_require = ['mock'],
From a1900665bde8510dedb7d4803d672ea6c16a70f3 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 20 Sep 2014 11:13:50 -0400
Subject: [PATCH 0035/1650] add requests to travis config to unbreak automated
build
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index d081b9302..7c3c9c3a1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,7 @@ python:
install:
- "python setup.py install"
- - "pip install curtsies greenlet pygments"
+ - "pip install curtsies greenlet pygments requests"
script:
- cd build/lib/
From 0eda1e13756cca9d0591caae23e8d023189d1a7a Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 20 Sep 2014 12:25:44 -0400
Subject: [PATCH 0036/1650] clarify Python 2/3, install instructions
---
doc/sphinx/source/contributing.rst | 39 +++++++++++++++++++-----------
1 file changed, 25 insertions(+), 14 deletions(-)
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index 131493fd6..a98d07e61 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -5,7 +5,7 @@ Contributing to bpython
Thanks for working on bpython!
-On the `GitHub issue tracker`_ some issues are labeled bite-size_
+On the `GitHub issue tracker`_ some issues are labeled bite-size_ -
these are particularly good ones to start out with.
See our section about the :ref:`community` for a list of resources.
@@ -16,27 +16,37 @@ to get a question answered depending on the time of day.
Getting your development environment set up
-------------------------------------------
+bpython supports Python 2.6, 2.7, 3.3 and 3.4. The code is written in Python
+2 and transformed for Python 3 with 2to3. This means that it's easier
+to work on bpython with Python 2 because it's you can use `python setup.py develop`
+(`development mode
+`_ installs
+by linking to the bpython source instead of copying it over)
+in a more straightforward way to have your changes immediately reflected by
+your bpython installation and the `bpython`, `bpython-curtsies`, and `bpython-urwid`
+commands.
+
Using a virtual environment is probably a good idea. Create a virtual environment with
.. code-block:: bash
- $ virtualenv bpython-dev
- $ source bpython-dev/bin/activate # this step is necssary every time you work on bpython
-
- $ deactivate # back to normal system environment
+ $ virtualenv bpython-dev # determines Python version used
+ $ source bpython-dev/bin/activate # necssary every time you work on bpython
-Fork `bpython` in the GitHub web interface, then clone the repo:
+Fork bpython in the GitHub web interface, then clone the repo:
.. code-block:: bash
$ git clone git@github.com:YOUR_GITHUB_USERNAME/bpython.git
+ $ # or "git clone https://github.com/YOUR_GITHUB_USERNAME/bpython.git"
-Next install this development version of `bpython`:
+Next install the dependencies and install your development copy of bpython:
.. code-block:: bash
- $ pip install pygments curtsies greenlet watchdog urwid # install all the dependencies
- $ pip install sphinx mock # development dependencies
+ $ pip install pygments requests # install required dependencies
+ $ pip install curtsies greenlet watchdog urwid # install optional dependencies
+ $ pip install sphinx mock nosetests # development dependencies
$ cd bpython
$ python setup.py develop
@@ -44,15 +54,17 @@ Next install this development version of `bpython`:
As a first dev task, I recommend getting `bpython` to print your name every time you hit a specific key.
-To run tests:
+To run tests from the bpython directory:
+
+.. code-block:: bash
- $ python -m unittest discover bpython
+ $ nosetests
To build the docs:
------------------
-The documentation is included in the regular `bpython` repository. After
-checking out the `bpython` repository and installing `sphinx` as described in
+The documentation is included in the bpython repository. After
+checking out the bpython repository and installing `sphinx` as described in
the previous step, you can run the following command in your checkout of the
repository to build the documentation:
@@ -63,7 +75,6 @@ repository to build the documentation:
Afterwards you can point your browser to `doc/sphinx/build/html/index.html`.
Don't forget to recreate the HTML after you make changes.
-
To hack on the site or theme
----------------------------
From a68c8bdf691def3f029359a0e7d89b1ae693a8fd Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sat, 20 Sep 2014 19:42:54 +0200
Subject: [PATCH 0037/1650] Add version information
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/configuration-options.rst | 2 ++
1 file changed, 2 insertions(+)
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index 6f2a06c82..d4391397b 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -71,6 +71,8 @@ pastebin_expiry
Time duration after which a paste should expire. Valid values are ``1day``,
``1week`` and ``1month``. (default: ``1week``)
+.. versionadded:: 0.14
+
pastebin_helper
^^^^^^^^^^^^^^^
From abd5342f3d5f5792af2c580a86a93cd2b2240437 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sat, 20 Sep 2014 19:43:08 +0200
Subject: [PATCH 0038/1650] Remove pastebin_private which is no longer needed
---
bpython/config.py | 4 ----
doc/sphinx/source/configuration-options.rst | 7 -------
2 files changed, 11 deletions(-)
diff --git a/bpython/config.py b/bpython/config.py
index 5949b78d9..38a3b7397 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -52,9 +52,7 @@ def loadini(struct, configfile):
'syntax': True,
'tab_length': 4,
'pastebin_confirm': True,
- 'pastebin_private': False,
'pastebin_url': 'https://bpaste.net/json/new',
- 'pastebin_private': True,
'pastebin_show_url': 'https://bpaste.net/show/$paste_id',
'pastebin_removal_url': 'https://bpaste.net/remove/$removal_id',
'pastebin_expiry': '1week',
@@ -142,9 +140,7 @@ def loadini(struct, configfile):
struct.help_key = config.get('keyboard', 'help')
struct.pastebin_confirm = config.getboolean('general', 'pastebin_confirm')
- struct.pastebin_private = config.getboolean('general', 'pastebin_private')
struct.pastebin_url = config.get('general', 'pastebin_url')
- struct.pastebin_private = config.get('general', 'pastebin_private')
struct.pastebin_show_url = config.get('general', 'pastebin_show_url')
struct.pastebin_removal_url = config.get('general', 'pastebin_removal_url')
struct.pastebin_expiry = config.get('general', 'pastebin_expiry')
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index d4391397b..d3cc1b7c9 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -47,13 +47,6 @@ The pastebin url to post to (without a trailing slash). This pastebin has to be
a pastebin which uses provides a similar interface to ``bpaste.net``'s JSON
interface. (default: https://bpaste.net/json/new)
-pastebin_private
-^^^^^^^^^^^^^^^^
-If the pastebin supports a private option to make a random paste id, use it.
-Default: True).
-
-.. versionadded:: 0.12
-
pastebin_show_url
^^^^^^^^^^^^^^^^^
The url under which the new paste can be reached. ``$paste_id`` will be replaced
From 51156aab776f82262bd7c4732de3cf3ab3e0dc63 Mon Sep 17 00:00:00 2001
From: David Branner
Date: Sat, 20 Sep 2014 07:26:57 -0400
Subject: [PATCH 0039/1650] correct typos
change case of original ^D
^D is a plausible command, but it does not work and ^d is meant.
make uniform different formats specifying defaults
---
doc/sphinx/source/authors.rst | 2 +-
doc/sphinx/source/bpdb.rst | 2 +-
doc/sphinx/source/community.rst | 6 +++---
doc/sphinx/source/configuration-options.rst | 10 +++++-----
doc/sphinx/source/contributing.rst | 2 +-
doc/sphinx/source/django.rst | 2 +-
6 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/doc/sphinx/source/authors.rst b/doc/sphinx/source/authors.rst
index 697474ef1..6c3ae627a 100644
--- a/doc/sphinx/source/authors.rst
+++ b/doc/sphinx/source/authors.rst
@@ -28,5 +28,5 @@ Other contributors are (in alphabetical order):
* Marien Zwart
A big thanks goes out to all the people who help us out by either submitting
-patches, helping us determine problems, our package maintainers and of course
+patches, helping us determine problems, our package maintainers, and of course
everybody who creates issues for us to fix.
diff --git a/doc/sphinx/source/bpdb.rst b/doc/sphinx/source/bpdb.rst
index e0b0174f1..eca017139 100644
--- a/doc/sphinx/source/bpdb.rst
+++ b/doc/sphinx/source/bpdb.rst
@@ -14,4 +14,4 @@ This will drop you into bpdb instead of pdb, which works exactly like pdb except
that you can additionally start bpython at the current stack frame by issuing
the command `Bpython` or `B`.
-You can exit bpython with `^D` to return to bpdb.
+You can exit bpython with `^d` to return to bpdb.
diff --git a/doc/sphinx/source/community.rst b/doc/sphinx/source/community.rst
index 76c8da1cd..2b946a48d 100644
--- a/doc/sphinx/source/community.rst
+++ b/doc/sphinx/source/community.rst
@@ -15,14 +15,14 @@ worry when you get no response (this does not usually happen) but we are all
from Europe and when you get to the channel during our nighttime you might have
to wait a while for a response.
-Mailinglist
+Mailing List
-----------
-We have a mailinglist at `google groups `_.
+We have a mailing list at `google groups `_.
You can post questions there and releases are announced on the mailing
list.
Website
-------
Our main website is http://bpython-interpreter.org/, our documentation can be
-found at http://docs.bpython-interpreter.org/ and our pastebin can be found at
+found at http://docs.bpython-interpreter.org/, and our pastebin can be found at
http://bpaste.net/.
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index d3cc1b7c9..29ee316e4 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -35,7 +35,7 @@ The time between lines before pastemode is activated in seconds (default: 0.02).
hist_length
^^^^^^^^^^^
-Number of lines to store in history (set to 0 to disable) (default: 100)
+Number of lines to store in history (set to 0 to disable) (default: 100).
tab_length
^^^^^^^^^^
@@ -45,24 +45,24 @@ pastebin_url
^^^^^^^^^^^^
The pastebin url to post to (without a trailing slash). This pastebin has to be
a pastebin which uses provides a similar interface to ``bpaste.net``'s JSON
-interface. (default: https://bpaste.net/json/new)
+interface (default: https://bpaste.net/json/new).
pastebin_show_url
^^^^^^^^^^^^^^^^^
The url under which the new paste can be reached. ``$paste_id`` will be replaced
-by the ID of the new paste. (default: https://bpaste.net/show/$paste_id/)
+by the ID of the new paste (default: https://bpaste.net/show/$paste_id/).
pastebin_removal_url
^^^^^^^^^^^^^^^^^^^^
The url under which a paste can be removed. ``$removal_id`` will be replaced
-by the removal ID of the paste. (default: https://bpaste.net/remova/$removal_id/)
+by the removal ID of the paste (default: https://bpaste.net/remova/$removal_id/).
.. versionadded:: 0.14
pastebin_expiry
^^^^^^^^^^^^^^^
Time duration after which a paste should expire. Valid values are ``1day``,
-``1week`` and ``1month``. (default: ``1week``)
+``1week`` and ``1month`` (default: ``1week``).
.. versionadded:: 0.14
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index 131493fd6..69a971ca1 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -67,7 +67,7 @@ Don't forget to recreate the HTML after you make changes.
To hack on the site or theme
----------------------------
-The site (and it's theme as well) is stored in a separate repository and built using
+The site (and its theme as well) is stored in a separate repository and built using
pelican. To start hacking on the site you need to start out with a checkout and
probably a virtual environment:
diff --git a/doc/sphinx/source/django.rst b/doc/sphinx/source/django.rst
index ec3f4d9bb..443649505 100644
--- a/doc/sphinx/source/django.rst
+++ b/doc/sphinx/source/django.rst
@@ -12,7 +12,7 @@ out of the box models and views for a lot of stuff.
For those people wanting to use bpython with their Django installation you can
follow the following steps. Written by Chanita Siridechkun. The following
instructions make bpython try to import a setting module in the current folder
-and let django set up it's enviroment with the settings module (if found) if
+and let django set up its enviroment with the settings module (if found) if
bpython can't find the settings module nothing happens and no enviroment gets
set up.
From c0bdca04c32ec9ffba0f26af43ed3ee924a57dfe Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Wed, 24 Sep 2014 13:59:50 -0400
Subject: [PATCH 0040/1650] Fix get_source_of_current_name to raise exceptions
instead of returning None
---
bpython/repl.py | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 221e7a176..2ac02b690 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -591,19 +591,16 @@ def get_source_of_current_name(self):
"""Return the source code of the object which is bound to the
current name in the current input line. Return `None` if the
source cannot be found."""
- try:
- obj = self.current_func
+ obj = self.current_func
+ if obj is None:
+ line = self.current_line
+ if line == "":
+ raise ValueError("Cannot get source of an empty string")
+ if inspection.is_eval_safe_name(line):
+ obj = self.get_object(line)
if obj is None:
- line = self.current_line
- if inspection.is_eval_safe_name(line):
- obj = self.get_object(line)
- if obj is None:
- return None
- source = inspect.getsource(obj)
- except (AttributeError, IOError, NameError, TypeError):
- return None
- else:
- return source
+ raise NameError("%s is not defined" % line)
+ return inspect.getsource(obj) #throws an exception that we'll catch
def set_docstring(self):
self.docstring = None
From d4dda9bca60757fe31fb8e8681559172210c56fc Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Wed, 24 Sep 2014 14:17:42 -0400
Subject: [PATCH 0041/1650] Fix p_key to catch exceptions from
get_source_of_current_name
---
bpython/cli.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index ebaebe809..bb7790301 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -968,14 +968,16 @@ def p_key(self, key):
return ''
elif key in key_dispatch[config.show_source_key]:
- source = self.get_source_of_current_name()
- if source is not None:
+ try:
+ source = self.get_source_of_current_name()
if config.highlight_show_source:
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
page(source)
- else:
- self.statusbar.message(_('Cannot show source.'))
+ except (ValueError, NameError), e:
+ self.statusbar.message(_(e))
+ except (AttributeError, IOError, TypeError), e:
+ self.statusbar.message(_('Failed to get source: %s' % e))
return ''
elif key in ('\n', '\r', 'PADENTER'):
From a4552db32357d73d8b2449b971d583d8e1935f6b Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Wed, 24 Sep 2014 14:46:54 -0400
Subject: [PATCH 0042/1650] Fix docstring for get_source_of_current_name to
match the new behavior
---
bpython/repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 2ac02b690..fc5f13d1b 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -589,7 +589,7 @@ def get_args(self):
def get_source_of_current_name(self):
"""Return the source code of the object which is bound to the
- current name in the current input line. Return `None` if the
+ current name in the current input line. Throw exception if the
source cannot be found."""
obj = self.current_func
if obj is None:
From f17eeec03ee7a0d91d84121af2162eca86e064f1 Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Wed, 24 Sep 2014 13:59:50 -0400
Subject: [PATCH 0043/1650] Fix get_source_of_current_name to raise exceptions
instead of returning None
---
bpython/repl.py | 21 +++++++++------------
1 file changed, 9 insertions(+), 12 deletions(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 883209509..58b0b5396 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -591,19 +591,16 @@ def get_source_of_current_name(self):
"""Return the source code of the object which is bound to the
current name in the current input line. Return `None` if the
source cannot be found."""
- try:
- obj = self.current_func
+ obj = self.current_func
+ if obj is None:
+ line = self.current_line
+ if line == "":
+ raise ValueError("Cannot get source of an empty string")
+ if inspection.is_eval_safe_name(line):
+ obj = self.get_object(line)
if obj is None:
- line = self.current_line
- if inspection.is_eval_safe_name(line):
- obj = self.get_object(line)
- if obj is None:
- return None
- source = inspect.getsource(obj)
- except (AttributeError, IOError, NameError, TypeError):
- return None
- else:
- return source
+ raise NameError("%s is not defined" % line)
+ return inspect.getsource(obj) #throws an exception that we'll catch
def set_docstring(self):
self.docstring = None
From 75e8e3593dd0f8b16084a264c09759f8d65cabae Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Wed, 24 Sep 2014 14:17:42 -0400
Subject: [PATCH 0044/1650] Fix p_key to catch exceptions from
get_source_of_current_name
---
bpython/cli.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index 78ee771d9..680b40f18 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -968,14 +968,16 @@ def p_key(self, key):
return ''
elif key in key_dispatch[config.show_source_key]:
- source = self.get_source_of_current_name()
- if source is not None:
+ try:
+ source = self.get_source_of_current_name()
if config.highlight_show_source:
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
page(source)
- else:
- self.statusbar.message(_('Cannot show source.'))
+ except (ValueError, NameError), e:
+ self.statusbar.message(_(e))
+ except (AttributeError, IOError, TypeError), e:
+ self.statusbar.message(_('Failed to get source: %s' % e))
return ''
elif key in ('\n', '\r', 'PADENTER'):
From ca74c4607809b895ed75d5e866f64c092dcae44e Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Wed, 24 Sep 2014 14:46:54 -0400
Subject: [PATCH 0045/1650] Fix docstring for get_source_of_current_name to
match the new behavior
---
bpython/repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 58b0b5396..3561faf87 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -589,7 +589,7 @@ def get_args(self):
def get_source_of_current_name(self):
"""Return the source code of the object which is bound to the
- current name in the current input line. Return `None` if the
+ current name in the current input line. Throw exception if the
source cannot be found."""
obj = self.current_func
if obj is None:
From 13eb5188dd3f408b28ab4e12fa964e7c1b10bc1a Mon Sep 17 00:00:00 2001
From: mlauter
Date: Wed, 24 Sep 2014 15:27:10 -0400
Subject: [PATCH 0046/1650] fix show_source method of curtsiesfrontend/repl to
catch exceptions from get_source_of_current_name
---
bpython/curtsiesfrontend/repl.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 1a2ed8e63..487bb3d21 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1300,13 +1300,16 @@ def pager(self, text):
self.focus_on_subprocess(command + [tmp.name])
def show_source(self):
- source = self.get_source_of_current_name()
- if source is None:
- self.status_bar.message(_('Cannot show source.'))
- else:
+ try:
+ source = self.get_source_of_current_name()
if self.config.highlight_show_source:
- source = format(PythonLexer().get_tokens(source), TerminalFormatter())
+ source = format(PythonLexer().get_tokens(source),
+ TerminalFormatter())
self.pager(source)
+ except (ValueError, NameError), e:
+ self.status_bar.message(_(e))
+ except (AttributeError, IOError, TypeError), e:
+ self.status_bar.message(_('Failed to get source: %s' % e))
def help_text(self):
return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
From 31db4c7e27bc5896ed59562b9c53683397221094 Mon Sep 17 00:00:00 2001
From: mlauter
Date: Wed, 24 Sep 2014 16:10:58 -0400
Subject: [PATCH 0047/1650] modify the NameError message displayed by
show_source
---
bpython/cli.py | 4 +++-
bpython/curtsiesfrontend/repl.py | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index 680b40f18..4579545f8 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -974,8 +974,10 @@ def p_key(self, key):
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
page(source)
- except (ValueError, NameError), e:
+ except (ValueError), e:
self.statusbar.message(_(e))
+ except (NameError), e:
+ self.statusbar.message(_('Cannot get source: %s' % e))
except (AttributeError, IOError, TypeError), e:
self.statusbar.message(_('Failed to get source: %s' % e))
return ''
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 487bb3d21..6fd1a3470 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1306,8 +1306,10 @@ def show_source(self):
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
self.pager(source)
- except (ValueError, NameError), e:
+ except (ValueError), e:
self.status_bar.message(_(e))
+ except (NameError), e:
+ self.status_bar.message(_('Cannot get source: %s' % e))
except (AttributeError, IOError, TypeError), e:
self.status_bar.message(_('Failed to get source: %s' % e))
From 1c218a18b5aaa773a6f32c4fa0c69122638032fb Mon Sep 17 00:00:00 2001
From: mlauter
Date: Wed, 24 Sep 2014 16:11:43 -0400
Subject: [PATCH 0048/1650] send long error messages originating in
inspect.getsource to pager not status_bar
---
bpython/cli.py | 2 +-
bpython/curtsiesfrontend/repl.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index 4579545f8..23484ced0 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -979,7 +979,7 @@ def p_key(self, key):
except (NameError), e:
self.statusbar.message(_('Cannot get source: %s' % e))
except (AttributeError, IOError, TypeError), e:
- self.statusbar.message(_('Failed to get source: %s' % e))
+ self.pager.message(_('Failed to get source: %s' % e))
return ''
elif key in ('\n', '\r', 'PADENTER'):
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 6fd1a3470..567363834 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1311,7 +1311,7 @@ def show_source(self):
except (NameError), e:
self.status_bar.message(_('Cannot get source: %s' % e))
except (AttributeError, IOError, TypeError), e:
- self.status_bar.message(_('Failed to get source: %s' % e))
+ self.pager(_('Failed to get source: %s' % e))
def help_text(self):
return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
From dab1ccbe62c2639dbd5aa3acb151909797e995db Mon Sep 17 00:00:00 2001
From: mlauter
Date: Wed, 24 Sep 2014 17:05:28 -0400
Subject: [PATCH 0049/1650] revert to shortening messages thrown by
inspect.getsource before raising
---
bpython/repl.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 3561faf87..39885d789 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -600,7 +600,15 @@ def get_source_of_current_name(self):
obj = self.get_object(line)
if obj is None:
raise NameError("%s is not defined" % line)
- return inspect.getsource(obj) #throws an exception that we'll catch
+ try:
+ inspect.getsource(obj)
+ except TypeError, e:
+ msg = e.msg
+ if "built-in" in msg:
+ msg.split(">")
+ raise TypeError("Cannot access source of " % self.current_line)
+ else:
+ raise TypeError("No source code found for %s" % self.current_line)
def set_docstring(self):
self.docstring = None
From 73d74b2463354a826348a4efe804141af99d699b Mon Sep 17 00:00:00 2001
From: mlauter
Date: Wed, 24 Sep 2014 17:15:50 -0400
Subject: [PATCH 0050/1650] simplify error message raising and handling
---
bpython/cli.py | 4 +---
bpython/curtsiesfrontend/repl.py | 6 ++----
bpython/repl.py | 6 ++----
3 files changed, 5 insertions(+), 11 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index 23484ced0..5ba09d44e 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -974,12 +974,10 @@ def p_key(self, key):
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
page(source)
- except (ValueError), e:
+ except (ValueError, AttributeError, IOError, TypeError), e:
self.statusbar.message(_(e))
except (NameError), e:
self.statusbar.message(_('Cannot get source: %s' % e))
- except (AttributeError, IOError, TypeError), e:
- self.pager.message(_('Failed to get source: %s' % e))
return ''
elif key in ('\n', '\r', 'PADENTER'):
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 567363834..79924dac1 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1306,13 +1306,11 @@ def show_source(self):
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
self.pager(source)
- except (ValueError), e:
+ except (ValueError, AttributeError, IOError, TypeError), e:
self.status_bar.message(_(e))
except (NameError), e:
self.status_bar.message(_('Cannot get source: %s' % e))
- except (AttributeError, IOError, TypeError), e:
- self.pager(_('Failed to get source: %s' % e))
-
+
def help_text(self):
return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
diff --git a/bpython/repl.py b/bpython/repl.py
index 39885d789..3130bb5aa 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -598,14 +598,12 @@ def get_source_of_current_name(self):
raise ValueError("Cannot get source of an empty string")
if inspection.is_eval_safe_name(line):
obj = self.get_object(line)
- if obj is None:
- raise NameError("%s is not defined" % line)
+
try:
inspect.getsource(obj)
except TypeError, e:
- msg = e.msg
+ msg = e.message
if "built-in" in msg:
- msg.split(">")
raise TypeError("Cannot access source of " % self.current_line)
else:
raise TypeError("No source code found for %s" % self.current_line)
From 2f9c8cf2edd5cf59ed9f13cb177bb367e53d3662 Mon Sep 17 00:00:00 2001
From: Liudmila
Date: Thu, 25 Sep 2014 15:04:31 -0400
Subject: [PATCH 0051/1650] Change error message for empty string
---
bpython/repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 7809d2ab0..d8f2e8799 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -595,7 +595,7 @@ def get_source_of_current_name(self):
if obj is None:
line = self.current_line
if line == "":
- raise ValueError("Cannot get source of an empty string")
+ raise ValueError("Nothing to get source of")
if inspection.is_eval_safe_name(line):
obj = self.get_object(line)
try:
From e7630aceb767ce79e9ccf01c3b43a547c0d029c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Fri, 26 Sep 2014 20:37:45 +0200
Subject: [PATCH 0052/1650] Use the runsource method of interpreter object in
bpython.args.exec_code().
__future__ imports affect the compilation of subsequent code. The interpreter
object's compile method keeps track of which __future__ imports it has
already seen, while the built-in compile function doesn't.
Closes #369.
---
bpython/args.py | 4 ++--
bpython/test/test_curtsies_repl.py | 1 -
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/bpython/args.py b/bpython/args.py
index dcf3c7b57..ff026357b 100644
--- a/bpython/args.py
+++ b/bpython/args.py
@@ -104,8 +104,8 @@ def exec_code(interpreter, args):
sys.argv
"""
with open(args[0], 'r') as sourcefile:
- code_obj = compile(sourcefile.read(), args[0], 'exec')
+ source = sourcefile.read()
old_argv, sys.argv = sys.argv, args
sys.path.insert(0, os.path.abspath(os.path.dirname(args[0])))
- interpreter.runcode(code_obj)
+ interpreter.runsource(source, args[0], 'exec')
sys.argv = old_argv
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index ab496be2a..3bcc21416 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -99,7 +99,6 @@ def test_repl(self):
repl.push('1 / 2')
self.assertEqual(out.getvalue(), '0.5\n')
- @skip('Failing - this is issue #369')
def test_interactive(self):
interp = code.InteractiveInterpreter(locals={})
with captured_output() as (out, err):
From 9a6f047e0c74f5bcd24e7317d95aaae364067252 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 26 Sep 2014 18:11:52 -0400
Subject: [PATCH 0053/1650] fix mistakes trundle pointed out
---
bpython/test/test_args.py | 17 -----------------
bpython/test/test_curtsies_repl.py | 2 +-
2 files changed, 1 insertion(+), 18 deletions(-)
delete mode 100644 bpython/test/test_args.py
diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py
deleted file mode 100644
index 1d5edc7f7..000000000
--- a/bpython/test/test_args.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import os
-import sys
-import unittest
-from mock import Mock, MagicMock
-try:
- from unittest import skip
-except ImportError:
- def skip(f):
- return lambda self: None
-
-from bpython import config, repl, cli, autocomplete
-
-class TestFutureImports(unittest.TestCase):
-
- def test_interactive(self):
- pass
-
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index 3bcc21416..b80ac430a 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -23,7 +23,7 @@ def setup_config(conf):
config.loadini(config_struct, os.devnull)
for key, value in conf.items():
if not hasattr(config_struct, key):
- raise ValueError("%r is not a valid config attribute", (key,))
+ raise ValueError("%r is not a valid config attribute" % (key,))
setattr(config_struct, key, value)
return config_struct
From f05521c3a15ddad2358baff2b011b2f8b3d6931e Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Mon, 29 Sep 2014 14:02:37 +0200
Subject: [PATCH 0054/1650] Add AppData file
Thanks to Richard Hughes and Ryan Lerch for the initial file and forwarding it.
We use the description from our homepage instead and also our screenshots.
Signed-off-by: Sebastian Ramacher
---
MANIFEST.in | 1 +
data/bpython.appdata.xml | 37 +++++++++++++++++++++++++++++++++++++
setup.py | 4 +++-
3 files changed, 41 insertions(+), 1 deletion(-)
create mode 100644 data/bpython.appdata.xml
diff --git a/MANIFEST.in b/MANIFEST.in
index 73b6ab574..45293b64a 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -4,6 +4,7 @@ include CHANGELOG
include LICENSE
include data/bpython
include data/bpython.desktop
+include data/bpython.appdata.xml
include doc/sphinx/source/conf.py
include doc/sphinx/source/*.rst
include doc/sphinx/source/logo.png
diff --git a/data/bpython.appdata.xml b/data/bpython.appdata.xml
new file mode 100644
index 000000000..b24c09368
--- /dev/null
+++ b/data/bpython.appdata.xml
@@ -0,0 +1,37 @@
+
+
+
+
+ bpython.desktop
+ CC0-1.0
+ MIT
+ bpython interpreter
+ Fancy interface for the Python interpreter
+
+
+ bpython is a fancy interface to the Python interpreter. It has the
+ following features:
+
+ - In-line syntac highlighting.
+ - Readline-like autocomplete with suggesstion displayed as you type.
+ - Expected parameter list for any Python function.
+ - "Rewind" function to pop the last line of code from memory and re-evaluate.
+ - Send the code you've entered off to a pastebin.
+ - Save the code you've entered to a file.
+ - Auto-indentation.
+
+
+
+ http://www.bpython-interpreter.org/
+
+ http://bpython-interpreter.org/images/8.png
+ http://bpython-interpreter.org/images/1.png
+ http://bpython-interpreter.org/images/2.png
+ http://bpython-interpreter.org/images/3.png
+ http://bpython-interpreter.org/images/4.png
+ http://bpython-interpreter.org/images/5.png
+ http://bpython-interpreter.org/images/6.png
+ http://bpython-interpreter.org/images/7.png
+
+ bpython@googlegroups.com
+
diff --git a/setup.py b/setup.py
index 83fa8ab7f..05c218807 100755
--- a/setup.py
+++ b/setup.py
@@ -129,7 +129,9 @@ def initialize_options(self):
data_files = [
# desktop shortcut
- (os.path.join('share', 'applications'), ['data/bpython.desktop'])
+ (os.path.join('share', 'applications'), ['data/bpython.desktop']),
+ # AppData
+ (os.path.join('share', 'appdata'), ['data/bpython.appdata.xml'])
]
data_files.extend(man_pages)
From 1684cbf46040a460e74006ceffb97d1046a5df19 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Mon, 29 Sep 2014 14:05:49 +0200
Subject: [PATCH 0055/1650] Ignore Sphinx build artefacts
Signed-off-by: Sebastian Ramacher
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 5cbac053e..62cb91fb7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ build/*
env
.DS_Store
.idea/
+doc/sphinx/build/*
From 80d35b49913312885540e91e556e0f93478c1593 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Tue, 30 Sep 2014 13:52:14 +0200
Subject: [PATCH 0056/1650] Use text mode for temporary Python file in tests.
Thanks @myint
---
bpython/test/test_curtsies_repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index b80ac430a..d2e545f75 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -102,7 +102,7 @@ def test_repl(self):
def test_interactive(self):
interp = code.InteractiveInterpreter(locals={})
with captured_output() as (out, err):
- with tempfile.NamedTemporaryFile(suffix='.py') as f:
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f:
f.write('from __future__ import division\n')
f.write('print 1/2\n')
f.flush()
From fd889b38bc17bf00be3237ba016767a4b013dde5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Wed, 1 Oct 2014 11:04:45 +0200
Subject: [PATCH 0057/1650] Small Python 3 fix for issue #369's test.
---
bpython/test/test_curtsies_repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index d2e545f75..a83200607 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -104,7 +104,7 @@ def test_interactive(self):
with captured_output() as (out, err):
with tempfile.NamedTemporaryFile(mode='w', suffix='.py') as f:
f.write('from __future__ import division\n')
- f.write('print 1/2\n')
+ f.write('print(1/2)\n')
f.flush()
args.exec_code(interp, [f.name])
From 86fb57040d9e1313a5209fe931e03b06fc3e05ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sat, 4 Oct 2014 19:07:09 +0200
Subject: [PATCH 0058/1650] Remove some unused imports.
---
bpython/cli.py | 1 -
bpython/curtsies.py | 2 --
bpython/curtsiesfrontend/interpreter.py | 5 ++---
bpython/curtsiesfrontend/replpainter.py | 3 +--
bpython/repl.py | 2 --
bpython/test/test_autocomplete.py | 2 --
bpython/test/test_curtsies_painting.py | 1 -
bpython/test/test_manual_readline.py | 3 ---
8 files changed, 3 insertions(+), 16 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index 5ba09d44e..aa7a7621e 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -84,7 +84,6 @@
from bpython import repl
from bpython._py3compat import py3
from bpython.pager import page
-from bpython import autocomplete
import bpython.args
if not py3:
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index d4dc29d07..2b6fc3775 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -4,10 +4,8 @@
import logging
import sys
import time
-from subprocess import Popen, PIPE
from optparse import Option
from itertools import izip
-from functools import wraps
import curtsies
import curtsies.window
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index 7013b436b..3be4a25f2 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -1,13 +1,12 @@
import code
import traceback
import sys
-from pygments.style import Style
from pygments.token import *
from pygments.formatter import Formatter
from curtsies.bpythonparse import parse
-from codeop import CommandCompiler, compile_command
+from codeop import CommandCompiler
from pygments.lexers import get_lexer_by_name
-from pygments.styles import get_style_by_name
+
default_colors = {
Generic.Error:'R',
diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py
index 56d418e1c..e24467657 100644
--- a/bpython/curtsiesfrontend/replpainter.py
+++ b/bpython/curtsiesfrontend/replpainter.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
import logging
-import os
from curtsies import fsarray, fmtstr
from curtsies.bpythonparse import func_for_letter
diff --git a/bpython/repl.py b/bpython/repl.py
index d8f2e8799..991181f6e 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -26,7 +26,6 @@
import codecs
import errno
import inspect
-import logging
import os
import pydoc
import requests
@@ -39,7 +38,6 @@
import unicodedata
from itertools import takewhile
from locale import getpreferredencoding
-from socket import error as SocketError
from string import Template
from urllib import quote as urlquote
from urlparse import urlparse, urljoin
diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py
index 203297120..fc47a251a 100644
--- a/bpython/test/test_autocomplete.py
+++ b/bpython/test/test_autocomplete.py
@@ -1,6 +1,4 @@
from bpython import autocomplete
-from functools import partial
-import inspect
import unittest
try:
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index b27693a93..e4736abee 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -1,5 +1,4 @@
# coding: utf8
-import unittest
import sys
import os
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index 9cd6c286b..8bb6eb0da 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -1,6 +1,3 @@
-
-from collections import namedtuple
-
from bpython.curtsiesfrontend.manual_readline import *
import unittest
From 2ce4a87f90a87c0972c6e436f43ef4e7f5ba06be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sat, 4 Oct 2014 19:10:25 +0200
Subject: [PATCH 0059/1650] Minor cosmetic changes.
---
bpython/autocomplete.py | 4 ++--
bpython/cli.py | 2 +-
bpython/curtsiesfrontend/repl.py | 8 ++++----
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index 4071e2c13..c78d1e25b 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -341,9 +341,9 @@ def safe_eval(expr, namespace):
try:
obj = eval(expr, namespace)
return obj
- except (NameError, AttributeError, SyntaxError) as e:
+ except (NameError, AttributeError, SyntaxError):
# If debugging safe_eval, raise this!
- # raise e
+ # raise
return SafeEvalFailed
def attr_matches(text, namespace, autocomplete_mode):
diff --git a/bpython/cli.py b/bpython/cli.py
index aa7a7621e..ea8eaf9a7 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -975,7 +975,7 @@ def p_key(self, key):
page(source)
except (ValueError, AttributeError, IOError, TypeError), e:
self.statusbar.message(_(e))
- except (NameError), e:
+ except NameError, e:
self.statusbar.message(_('Cannot get source: %s' % e))
return ''
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 79924dac1..086a67926 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -349,9 +349,9 @@ def new_import(name, globals={}, locals={}, fromlist=[], level=-1):
if name in old_module_locations:
loc = old_module_locations[name]
if self.watching_files:
- self.watcher.add_module(old_module_locations[name])
+ self.watcher.add_module(loc)
else:
- self.watcher.add_module_later(old_module_locations[name])
+ self.watcher.add_module_later(loc)
raise
else:
if hasattr(m, "__file__"):
@@ -1308,9 +1308,9 @@ def show_source(self):
self.pager(source)
except (ValueError, AttributeError, IOError, TypeError), e:
self.status_bar.message(_(e))
- except (NameError), e:
+ except NameError, e:
self.status_bar.message(_('Cannot get source: %s' % e))
-
+
def help_text(self):
return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
From af64bb131607c0560b21a0eb49cdf1a158204161 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sat, 4 Oct 2014 19:34:31 +0200
Subject: [PATCH 0060/1650] Remove some duplicated classes in
bpython.autocompletion.
---
bpython/autocomplete.py | 40 ----------------------------------------
1 file changed, 40 deletions(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index c78d1e25b..73868c51f 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -228,46 +228,6 @@ def matches(cls, cursor_offset, line, full_code, **kwargs):
start, end, word = r
return [name for name in MAGIC_METHODS if name.startswith(word)]
-class GlobalCompletion(BaseCompletionType):
- @classmethod
- def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
- r = cls.locate(cursor_offset, line)
- if r is None:
- return None
- start, end, word = r
- return global_matches(word, locals_, mode)
- locate = staticmethod(lineparts.current_single_word)
-
-class ParameterNameCompletion(BaseCompletionType):
- @classmethod
- def matches(cls, cursor_offset, line, argspec, **kwargs):
- if not argspec:
- return None
- r = cls.locate(cursor_offset, line)
- if r is None:
- return None
- start, end, word = r
- if argspec:
- matches = [name + '=' for name in argspec[1][0]
- if isinstance(name, basestring) and name.startswith(word)]
- if py3:
- matches.extend(name + '=' for name in argspec[1][4]
- if name.startswith(word))
- return matches
- locate = staticmethod(lineparts.current_word)
-
-class MagicMethodCompletion(BaseCompletionType):
- locate = staticmethod(lineparts.current_method_definition_name)
- @classmethod
- def matches(cls, cursor_offset, line, full_code, **kwargs):
- r = cls.locate(cursor_offset, line)
- if r is None:
- return None
- if 'class' not in full_code:
- return None
- start, end, word = r
- return [name for name in MAGIC_METHODS if name.startswith(word)]
-
class GlobalCompletion(BaseCompletionType):
@classmethod
def matches(cls, cursor_offset, line, locals_, mode, **kwargs):
From 236e320cec752e5afa27f84a2fb699d52fbe35a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sat, 4 Oct 2014 19:37:30 +0200
Subject: [PATCH 0061/1650] Fix duplicated test case name.
---
bpython/test/test_line_properties.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py
index 6131e0755..0542b1161 100644
--- a/bpython/test/test_line_properties.py
+++ b/bpython/test/test_line_properties.py
@@ -245,9 +245,10 @@ def test_simple(self):
self.assertAccess(' def bar(x, y)|:')
self.assertAccess(' def (x, y)')
-class TestMethodDefinitionName(LineTestCase):
+class TestSingleWord(LineTestCase):
def setUp(self):
self.func = current_single_word
+
def test_simple(self):
self.assertAccess('foo.bar|')
self.assertAccess('.foo|')
From 7e51b518de3dace1f40e52e7dba996de0bb48e65 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sun, 5 Oct 2014 16:13:41 +0200
Subject: [PATCH 0062/1650] Python 3 fixes for urwid. Closes #243.
---
bpython/urwid.py | 26 ++++++++++++++++++--------
1 file changed, 18 insertions(+), 8 deletions(-)
diff --git a/bpython/urwid.py b/bpython/urwid.py
index 582b551e6..cb82b09ae 100644
--- a/bpython/urwid.py
+++ b/bpython/urwid.py
@@ -80,7 +80,9 @@
'd': 'default',
}
-# Add our keys to the urwid command_map
+
+def getpreferredencoding():
+ return locale.getpreferredencoding() or "ascii"
try:
@@ -746,7 +748,8 @@ def _populate_completion(self):
func_name, args, is_bound, in_arg = self.argspec
args, varargs, varkw, defaults = args[:4]
if py3:
- kwonly, kwonly_defaults = args[4:]
+ kwonly = self.argspec[1][4]
+ kwonly_defaults = self.argspec[1][5] or {}
else:
kwonly, kwonly_defaults = [], {}
markup = [('bold name', func_name),
@@ -972,14 +975,17 @@ def prompt(self, more):
# We need the caption to use unicode as urwid normalizes later
# input to be the same type, using ascii as encoding. If the
# caption is bytes this breaks typing non-ascii into bpython.
- # Currently this decodes using ascii as I do not know where
- # ps1 is getting loaded from. If anyone wants to make
- # non-ascii prompts work feel free to fix this.
if not more:
- caption = ('prompt', self.ps1.decode('ascii'))
+ if py3:
+ caption = ('prompt', self.ps1)
+ else:
+ caption = ('prompt', self.ps1.decode(getpreferredencoding()))
self.stdout_hist += self.ps1
else:
- caption = ('prompt_more', self.ps2.decode('ascii'))
+ if py3:
+ caption = ('prompt_more', self.ps2)
+ else:
+ caption = ('prompt_more', self.ps2.decode(getpreferredencoding()))
self.stdout_hist += self.ps2
self.edit = BPythonEdit(self.config, caption=caption)
@@ -1024,7 +1030,11 @@ def handle_input(self, event):
self.history.append(inp)
self.edit.make_readonly()
# XXX what is this s_hist thing?
- self.stdout_hist += inp.encode(locale.getpreferredencoding()) + '\n'
+ if py3:
+ self.stdout_hist += inp
+ else:
+ self.stdout_hist += inp.encode(locale.getpreferredencoding())
+ self.stdout_hist += '\n'
self.edit = None
# This may take a while, so force a redraw first:
self.main_loop.draw_screen()
From 28cb9f2c2a551afb11a57f3a13575bee0dfc33de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sun, 12 Oct 2014 19:12:24 +0200
Subject: [PATCH 0063/1650] Add missing return statement. Fixes #403.
---
bpython/repl.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 991181f6e..059342721 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -596,8 +596,8 @@ def get_source_of_current_name(self):
raise ValueError("Nothing to get source of")
if inspection.is_eval_safe_name(line):
obj = self.get_object(line)
- try:
- inspect.getsource(obj)
+ try:
+ return inspect.getsource(obj)
except TypeError, e:
msg = e.message
if "built-in" in msg:
From e18e43787bfe26f58c503cc7738c7336409f1150 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Sun, 12 Oct 2014 20:06:40 +0200
Subject: [PATCH 0064/1650] Use a specific `SourceNotFound` in
get_source_of_current_name().
Instead of leaking all possible exceptions (that the caller has to deal
with), use a single `SourceNotFound` exception.
---
bpython/cli.py | 7 +++----
bpython/curtsiesfrontend/repl.py | 9 ++++-----
bpython/repl.py | 32 ++++++++++++++++++++------------
3 files changed, 27 insertions(+), 21 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index ea8eaf9a7..46db9d7e1 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -969,14 +969,13 @@ def p_key(self, key):
elif key in key_dispatch[config.show_source_key]:
try:
source = self.get_source_of_current_name()
+ except repl.SourceNotFound, e:
+ self.statusbar.message(_(e))
+ else:
if config.highlight_show_source:
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
page(source)
- except (ValueError, AttributeError, IOError, TypeError), e:
- self.statusbar.message(_(e))
- except NameError, e:
- self.statusbar.message(_('Cannot get source: %s' % e))
return ''
elif key in ('\n', '\r', 'PADENTER'):
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 086a67926..d827d55b1 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -29,7 +29,7 @@
from curtsies import events
import bpython
-from bpython.repl import Repl as BpythonRepl
+from bpython.repl import Repl as BpythonRepl, SourceNotFound
from bpython.config import Struct, loadini, default_config_path
from bpython.formatter import BPythonFormatter
from bpython import autocomplete, importcompletion
@@ -1302,14 +1302,13 @@ def pager(self, text):
def show_source(self):
try:
source = self.get_source_of_current_name()
+ except SourceNotFound, e:
+ self.status_bar.message(_(e))
+ else:
if self.config.highlight_show_source:
source = format(PythonLexer().get_tokens(source),
TerminalFormatter())
self.pager(source)
- except (ValueError, AttributeError, IOError, TypeError), e:
- self.status_bar.message(_(e))
- except NameError, e:
- self.status_bar.message(_('Cannot get source: %s' % e))
def help_text(self):
return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
diff --git a/bpython/repl.py b/bpython/repl.py
index 059342721..6abc07ec3 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -377,6 +377,10 @@ def file_prompt(self, s):
raise NotImplementedError
+class SourceNotFound(Exception):
+ """Exception raised when the requested source could not be found."""
+
+
class Repl(object):
"""Implements the necessary guff for a Python-repl-alike interface
@@ -587,23 +591,27 @@ def get_args(self):
def get_source_of_current_name(self):
"""Return the source code of the object which is bound to the
- current name in the current input line. Throw exception if the
+ current name in the current input line. Throw `SourceNotFound` if the
source cannot be found."""
obj = self.current_func
- if obj is None:
- line = self.current_line
- if line == "":
- raise ValueError("Nothing to get source of")
- if inspection.is_eval_safe_name(line):
- obj = self.get_object(line)
try:
- return inspect.getsource(obj)
+ if obj is None:
+ line = self.current_line
+ if not line.strip():
+ raise SourceNotFound("Nothing to get source of")
+ if inspection.is_eval_safe_name(line):
+ obj = self.get_object(line)
+ return inspect.getsource(obj)
+ except (AttributeError, NameError), e:
+ msg = "Cannot get source: " + str(e)
+ except IOError, e:
+ msg = str(e)
except TypeError, e:
- msg = e.message
- if "built-in" in msg:
- raise TypeError("Cannot access source of " % self.current_line)
+ if "built-in" in str(e):
+ msg = "Cannot access source of %r" % (obj, )
else:
- raise TypeError("No source code found for %s" % self.current_line)
+ msg = "No source code found for %s" % (self.current_line, )
+ raise SourceNotFound(msg)
def set_docstring(self):
self.docstring = None
From 6dcfc3794c89b9ad1e99bdd2a72f6e041b6f57e3 Mon Sep 17 00:00:00 2001
From: lrraymond13
Date: Tue, 14 Oct 2014 15:50:12 -0400
Subject: [PATCH 0065/1650] Changed 'scroll' to 'curtsies' in help message
---
bpython/curtsies.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 2b6fc3775..24fe1ceae 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -23,7 +23,7 @@
def main(args=None, locals_=None, banner=None):
config, options, exec_args = bpargs.parse(args, (
- 'scroll options', None, [
+ 'curtsies options', None, [
Option('--log', '-L', action='store_true',
help=_("log debug messages to bpython.log")),
Option('--type', '-t', action='store_true',
From df80ea147b882b74e0830a901db424aadc7cdeba Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Thu, 11 Sep 2014 17:17:37 -0400
Subject: [PATCH 0066/1650] pressing prints the last word of the last
command to the command line Added test for get last word
---
bpython/curtsiesfrontend/repl.py | 25 ++++++++++++++++++++++++-
bpython/test/test_curtsies_repl.py | 9 +++++++++
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 419c9fce1..fd22d9802 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -46,6 +46,7 @@
#TODO other autocomplete modes (also fix in other bpython implementations)
+
from curtsies.configfile_keynames import keymap as key_dispatch
logger = logging.getLogger(__name__)
@@ -87,6 +88,7 @@ def __init__(self, coderunner, repl, configured_edit_keys=None):
def process_event(self, e):
assert self.has_focus
+
logger.debug('fake input processing event %r', e)
if isinstance(e, events.PasteEvent):
for ee in e.events:
@@ -100,6 +102,9 @@ def process_event(self, e):
self.current_line = ''
self.cursor_offset = 0
self.repl.run_code_and_maybe_finish()
+ elif e in ("",):
+ self.get_last_word()
+
elif e in [""]:
pass
elif e in ['']:
@@ -469,6 +474,8 @@ def process_key_event(self, e):
self.down_one_line()
elif e in ("",):
self.on_control_d()
+ elif e in ("",):
+ self.get_last_word()
elif e in ("",):
self.incremental_search(reverse=True)
elif e in ("",):
@@ -524,6 +531,21 @@ def process_key_event(self, e):
else:
self.add_normal_character(e)
+ def get_last_word(self):
+
+ def last_word(line):
+ if not line:
+ return ''
+ return line.split().pop()
+
+ previous_word = last_word(self.rl_history.entry)
+ word = last_word(self.rl_history.back())
+ line=self.current_line
+ self._set_current_line(line[:len(line)-len(previous_word)]+word, reset_rl_history=False)
+
+ self._set_cursor_offset(self.cursor_offset-len(previous_word)+len(word), reset_rl_history=False)
+
+
def incremental_search(self, reverse=False, include_current=False):
if self.special_mode == None:
self.rl_history.enter(self.current_line)
@@ -808,7 +830,8 @@ def run_code_and_maybe_finish(self, for_code=None):
if err:
indent = 0
- #TODO This should be printed ABOVE the error that just happened instead
+
+ #TODO This should be printed ABOVE the error that just happened instead
# or maybe just thrown away and not shown
if self.current_stdouterr_line:
self.display_lines.extend(paint.display_linize(self.current_stdouterr_line, self.width))
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index b0c8a4c8d..21a2b3f57 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -51,6 +51,15 @@ def test_external_communication(self):
self.repl.send_current_block_to_external_editor()
self.repl.send_session_to_external_editor()
+ def test_get_last_word(self):
+ self.repl.rl_history.entries=['1','2 3','4 5 6']
+ self.repl._set_current_line('abcde')
+ self.repl.get_last_word()
+ self.assertEqual(self.repl.current_line,'abcde6')
+ self.repl.get_last_word()
+ self.assertEqual(self.repl.current_line,'abcde3')
+
+
@contextmanager # from http://stackoverflow.com/a/17981937/398212 - thanks @rkennedy
def captured_output():
new_out, new_err = StringIO(), StringIO()
From f721c1d96814ccae19ebaa5b76b6c1c4c146fe94 Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Wed, 1 Oct 2014 19:11:00 -0400
Subject: [PATCH 0067/1650] fixing bug 393
---
bpython/curtsiesfrontend/repl.py | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index fd22d9802..fff388d5d 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -308,6 +308,7 @@ def smarter_request_reload(desc):
self.stdin = FakeStdin(self.coderunner, self, self.edit_keys)
self.request_paint_to_clear_screen = False # next paint should clear screen
+ self.inconsistent_history = False # offscreen command yields different result from history
self.last_events = [None] * 50 # some commands act differently based on the prev event
# this list doesn't include instances of event.Event,
# only keypress-type events (no refresh screen events etc.)
@@ -1020,7 +1021,11 @@ def paint(self, about_to_exit=False, user_quit=False):
else:
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
-
+ if self.inconsistent_history == True:
+ msg = "#<---History inconsistent with output shown--->"
+ arr[0, 0:min(len(msg), width)] = [msg[:width]]
+ self.inconsistent_history = False
+"""FIX THE THING This if may need to go after the second one, but the while applies to both"""
if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
logger.debug('#<---History contiguity broken by rewind--->')
msg = "#<---History contiguity broken by rewind--->"
@@ -1216,10 +1221,13 @@ def reprint_line(self, lineno, tokens):
logger.debug("calling reprint line with %r %r", lineno, tokens)
if self.config.syntax:
self.display_buffer[lineno] = bpythonparse(format(tokens, self.formatter))
+
+ return self.display_lines
def reevaluate(self, insert_into_history=False):
"""bpython.Repl.undo calls this"""
if self.watcher: self.watcher.reset()
old_logical_lines = self.history
+ old_display_lines = self.display_lines
self.history = []
self.display_lines = []
@@ -1242,7 +1250,9 @@ def reevaluate(self, insert_into_history=False):
self.process_event(events.RefreshRequestEvent())
sys.stdin = self.stdin
self.reevaluating = False
-
+ num_lines_onscreen=len(self.lines_for_display)-max(0, self.scroll_offset)
+ if old_display_lines[:len(self.display_lines)-num_lines_onscreen]!=self.display_lines:
+ self.inconsistent_history = True
self.cursor_offset = 0
self.current_line = ''
From a8136bb3df14546f967c0f78d7b97c630b2746ef Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Tue, 7 Oct 2014 15:07:06 -0400
Subject: [PATCH 0068/1650] rewinding some of our changes.
---
bpython/curtsies.py | 4 ++--
bpython/curtsiesfrontend/repl.py | 35 +++++++++++++++++++++-----------
2 files changed, 25 insertions(+), 14 deletions(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index d4dc29d07..a90df6031 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -33,10 +33,10 @@ def main(args=None, locals_=None, banner=None):
]))
if options.log:
handler = logging.FileHandler(filename='bpython.log')
- logging.getLogger('curtsies').setLevel(logging.DEBUG)
+ logging.getLogger('curtsies').setLevel(logging.WARNING)
logging.getLogger('curtsies').addHandler(handler)
logging.getLogger('curtsies').propagate = False
- logging.getLogger('bpython').setLevel(logging.DEBUG)
+ logging.getLogger('bpython').setLevel(logging.WARNING)
logging.getLogger('bpython').addHandler(handler)
logging.getLogger('bpython').propagate = False
else:
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index fff388d5d..a470e5700 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1021,15 +1021,23 @@ def paint(self, about_to_exit=False, user_quit=False):
else:
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
- if self.inconsistent_history == True:
- msg = "#<---History inconsistent with output shown--->"
- arr[0, 0:min(len(msg), width)] = [msg[:width]]
- self.inconsistent_history = False
-"""FIX THE THING This if may need to go after the second one, but the while applies to both"""
- if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
- logger.debug('#<---History contiguity broken by rewind--->')
- msg = "#<---History contiguity broken by rewind--->"
- arr[0, 0:min(len(msg), width)] = [msg[:width]]
+ if self.inconsistent_history == False & current_line_start_row >= 0:
+ history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
+ arr[:history.height,:history.width] = history
+
+ else:
+ if self.inconsistent_history == True:
+ logger.debug("#<---History inconsistent with output shown--->")
+ msg = "#<---History inconsistent with output shown--->"
+ arr[0, 0:min(len(msg), width)] = [msg[:width]]
+ self.inconsistent_history = False
+ # self.scroll_offset -= 1
+ current_line_start_row = len(self.lines_for_display )- max(-1, self.scroll_offset)
+
+ if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
+ logger.debug('#<---History contiguity broken by rewind--->')
+ msg = "#<---History contiguity broken by rewind--->"
+ arr[0, 0:min(len(msg), width)] = [msg[:width]]
# move screen back up a screen minus a line
while current_line_start_row < 0:
@@ -1041,9 +1049,7 @@ def paint(self, about_to_exit=False, user_quit=False):
if arr.height <= min_height:
arr[min_height, 0] = ' ' # force scroll down to hide broken history message
- else:
- history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
- arr[:history.height,:history.width] = history
+
current_line = paint.paint_current_line(min_height, width, self.current_cursor_line)
if user_quit: # quit() or exit() in interp
@@ -1252,7 +1258,12 @@ def reevaluate(self, insert_into_history=False):
self.reevaluating = False
num_lines_onscreen=len(self.lines_for_display)-max(0, self.scroll_offset)
if old_display_lines[:len(self.display_lines)-num_lines_onscreen]!=self.display_lines:
+ old_display_lines_offscreen=old_display_lines[:len(self.display_lines)-num_lines_onscreen]
+ display_lines_offscreen=self.display_lines[:-num_lines_onscreen]
+
+ if old_display_lines_offscreen!=display_lines_offscreen:
self.inconsistent_history = True
+ self.scroll_offset=self.scroll_offset-max(-1,(len(old_display_lines_offscreen)-len(display_lines_offscreen)+1))
self.cursor_offset = 0
self.current_line = ''
From c427c71d2815bef7ada876e1c993cde39085d5f3 Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Tue, 7 Oct 2014 15:23:06 -0400
Subject: [PATCH 0069/1650] shows error message. However, jumps up 2 lines when
rewind is pressed the first time. Also does something weird when you try to
rewind past the error message
---
bpython/curtsiesfrontend/repl.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index a470e5700..7d9331c8d 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1257,13 +1257,17 @@ def reevaluate(self, insert_into_history=False):
sys.stdin = self.stdin
self.reevaluating = False
num_lines_onscreen=len(self.lines_for_display)-max(0, self.scroll_offset)
+ old_display_lines_offscreen=[]
+ display_lines_offscreen=[]
+
if old_display_lines[:len(self.display_lines)-num_lines_onscreen]!=self.display_lines:
- old_display_lines_offscreen=old_display_lines[:len(self.display_lines)-num_lines_onscreen]
- display_lines_offscreen=self.display_lines[:-num_lines_onscreen]
-
+ old_display_lines_offscreen=old_display_lines[:len(self.display_lines)-num_lines_onscreen]
+ display_lines_offscreen=self.display_lines[:-num_lines_onscreen]
+
if old_display_lines_offscreen!=display_lines_offscreen:
self.inconsistent_history = True
self.scroll_offset=self.scroll_offset-max(-1,(len(old_display_lines_offscreen)-len(display_lines_offscreen)+1))
+
self.cursor_offset = 0
self.current_line = ''
From ccd793f8c11b1fea485300508c9cef08cab92af7 Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Mon, 13 Oct 2014 19:06:58 -0400
Subject: [PATCH 0070/1650] History inconsistent message appears when history
offscreen changes
---
bpython/curtsiesfrontend/repl.py | 40 ++++++++++++++++++--------------
1 file changed, 23 insertions(+), 17 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 7d9331c8d..f6fd408b7 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -309,6 +309,7 @@ def smarter_request_reload(desc):
self.request_paint_to_clear_screen = False # next paint should clear screen
self.inconsistent_history = False # offscreen command yields different result from history
+ self.history_messed_up = True # history error message displayed
self.last_events = [None] * 50 # some commands act differently based on the prev event
# this list doesn't include instances of event.Event,
# only keypress-type events (no refresh screen events etc.)
@@ -1022,27 +1023,30 @@ def paint(self, about_to_exit=False, user_quit=False):
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
if self.inconsistent_history == False & current_line_start_row >= 0:
+ logger.debug("start %i",current_line_start_row)
history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
arr[:history.height,:history.width] = history
else:
if self.inconsistent_history == True:
- logger.debug("#<---History inconsistent with output shown--->")
- msg = "#<---History inconsistent with output shown--->"
- arr[0, 0:min(len(msg), width)] = [msg[:width]]
self.inconsistent_history = False
+ #if INCONSISTENT_HISTORY_MSG not in self.display_lines:
+ logger.debug(INCONSISTENT_HISTORY_MSG)
+ msg = INCONSISTENT_HISTORY_MSG
+ arr[0, 0:min(len(msg), width)] = [msg[:width]]
# self.scroll_offset -= 1
+
current_line_start_row = len(self.lines_for_display )- max(-1, self.scroll_offset)
if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
- logger.debug('#<---History contiguity broken by rewind--->')
- msg = "#<---History contiguity broken by rewind--->"
+ logger.debug(CONTIGUITY_BROKEN_MSG)
+ msg = CONTIGUITY_BROKEN_MSG
arr[0, 0:min(len(msg), width)] = [msg[:width]]
# move screen back up a screen minus a line
- while current_line_start_row < 0:
- self.scroll_offset = self.scroll_offset - self.height
- current_line_start_row = len(self.lines_for_display) - max(-1, self.scroll_offset)
+ while current_line_start_row < 0:
+ self.scroll_offset = self.scroll_offset - self.height
+ current_line_start_row = len(self.lines_for_display) - max(-1, self.scroll_offset)
history = paint.paint_history(max(0, current_line_start_row - 1), width, self.lines_for_display)
arr[1:history.height+1,:history.width] = history
@@ -1256,17 +1260,19 @@ def reevaluate(self, insert_into_history=False):
self.process_event(events.RefreshRequestEvent())
sys.stdin = self.stdin
self.reevaluating = False
- num_lines_onscreen=len(self.lines_for_display)-max(0, self.scroll_offset)
- old_display_lines_offscreen=[]
- display_lines_offscreen=[]
-
- if old_display_lines[:len(self.display_lines)-num_lines_onscreen]!=self.display_lines:
- old_display_lines_offscreen=old_display_lines[:len(self.display_lines)-num_lines_onscreen]
- display_lines_offscreen=self.display_lines[:-num_lines_onscreen]
+
+ num_lines_onscreen = len(self.lines_for_display) - max(0, self.scroll_offset)
+ old_display_lines_offscreen = []
+ display_lines_offscreen = []
+ if old_display_lines[:len(self.display_lines) - num_lines_onscreen]!= self.display_lines:
+ old_display_lines_offscreen = old_display_lines[:len(self.display_lines) - num_lines_onscreen]
+ display_lines_offscreen = self.display_lines[:-num_lines_onscreen]
- if old_display_lines_offscreen!=display_lines_offscreen:
+ if old_display_lines_offscreen != display_lines_offscreen:
+ self.scroll_offset=self.scroll_offset-(len(old_display_lines)-len(self.display_lines))
+
self.inconsistent_history = True
- self.scroll_offset=self.scroll_offset-max(-1,(len(old_display_lines_offscreen)-len(display_lines_offscreen)+1))
+ #self.scroll_offset = self.scroll_offset - max(-1,(len(old_display_lines_offscreen)-len(display_lines_offscreen)+1))
self.cursor_offset = 0
self.current_line = ''
From 2ebb430d50ea5df1e050c0a58156e965ad3de8b6 Mon Sep 17 00:00:00 2001
From: SusInMotion
Date: Mon, 13 Oct 2014 19:11:04 -0400
Subject: [PATCH 0071/1650] Inconsistent history message only shows up once
---
bpython/curtsiesfrontend/repl.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index f6fd408b7..30f2c0efa 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -309,7 +309,7 @@ def smarter_request_reload(desc):
self.request_paint_to_clear_screen = False # next paint should clear screen
self.inconsistent_history = False # offscreen command yields different result from history
- self.history_messed_up = True # history error message displayed
+ self.history_messed_up = False # history error message displayed
self.last_events = [None] * 50 # some commands act differently based on the prev event
# this list doesn't include instances of event.Event,
# only keypress-type events (no refresh screen events etc.)
@@ -1028,8 +1028,8 @@ def paint(self, about_to_exit=False, user_quit=False):
arr[:history.height,:history.width] = history
else:
- if self.inconsistent_history == True:
- self.inconsistent_history = False
+ if self.inconsistent_history == True and self.history_messed_up == False:
+ self.history_messed_up = True
#if INCONSISTENT_HISTORY_MSG not in self.display_lines:
logger.debug(INCONSISTENT_HISTORY_MSG)
msg = INCONSISTENT_HISTORY_MSG
@@ -1037,7 +1037,7 @@ def paint(self, about_to_exit=False, user_quit=False):
# self.scroll_offset -= 1
current_line_start_row = len(self.lines_for_display )- max(-1, self.scroll_offset)
-
+ self.inconsistent_history = False
if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
logger.debug(CONTIGUITY_BROKEN_MSG)
msg = CONTIGUITY_BROKEN_MSG
@@ -1268,7 +1268,7 @@ def reevaluate(self, insert_into_history=False):
old_display_lines_offscreen = old_display_lines[:len(self.display_lines) - num_lines_onscreen]
display_lines_offscreen = self.display_lines[:-num_lines_onscreen]
- if old_display_lines_offscreen != display_lines_offscreen:
+ if old_display_lines_offscreen != display_lines_offscreen and self.history_messed_up == False:
self.scroll_offset=self.scroll_offset-(len(old_display_lines)-len(self.display_lines))
self.inconsistent_history = True
From f4d353ce90d3c0d51db75a86ecafe66d4ebf0bf0 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 14 Oct 2014 22:12:23 -0400
Subject: [PATCH 0072/1650] Add tests for special rewind cases
---
bpython/curtsiesfrontend/repl.py | 4 +-
bpython/test/test_curtsies_painting.py | 150 ++++++++++++++++++++++++-
2 files changed, 150 insertions(+), 4 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 30f2c0efa..7837316ba 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -51,7 +51,9 @@
logger = logging.getLogger(__name__)
-HELP_MESSAGE = """
+INCONSISTENT_HISTORY_MSG = u"#<---History inconsistent with output shown--->"
+CONTIGUITY_BROKEN_MSG = u"#<---History contiguity broken by rewind--->"
+HELP_MESSAGE = u"""
Thanks for using bpython!
See http://bpython-interpreter.org/ for info, http://docs.bpython-interpreter.org/ for docs, and https://github.com/bpython/bpython for source.
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index b27693a93..e67d51cbf 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -2,14 +2,22 @@
import unittest
import sys
import os
+from contextlib import contextmanager
-from curtsies.formatstringarray import FormatStringTest, fsarray
+try:
+ from unittest import skip
+except ImportError:
+ def skip(f):
+ return lambda self: None
+from curtsies.formatstringarray import FormatStringTest, fsarray
from curtsies.fmtfuncs import *
+from curtsies.events import RefreshRequestEvent
from bpython import config
from bpython.curtsiesfrontend.repl import Repl
from bpython.repl import History
+from bpython.curtsiesfrontend.repl import INCONSISTENT_HISTORY_MSG, CONTIGUITY_BROKEN_MSG
def setup_config():
config_struct = config.Struct()
@@ -30,6 +38,9 @@ def assert_paint(self, screen, cursor_row_col):
def assert_paint_ignoring_formatting(self, screen, cursor_row_col):
array, cursor_pos = self.repl.paint()
self.assertFSArraysEqualIgnoringFormatting(array, screen)
+ self.assertEqual(cursor_pos, cursor_row_col)
+
+class TestCurtsiesPaintingSimple(TestCurtsiesPainting):
def test_startup(self):
screen = fsarray([cyan('>>> '), cyan('Welcome to')])
@@ -48,7 +59,7 @@ def test_run_line(self):
[self.repl.add_normal_character(c) for c in '1 + 1']
self.repl.on_enter()
screen = fsarray([u'>>> 1 + 1', '2', 'Welcome to'])
- self.assert_paint_ignoring_formatting(screen, (0, 9))
+ self.assert_paint_ignoring_formatting(screen, (1, 1))
finally:
sys.stdout = orig_stdout
@@ -62,4 +73,137 @@ def test_completion(self):
u'└───────────────────────┘',
u'',
u'Welcome to bpython! Press f']
- self.assert_paint_ignoring_formatting(screen, (0, 9))
+ self.assert_paint_ignoring_formatting(screen, (0, 4))
+
+@contextmanager
+def output_to_repl(repl):
+ old_out, old_err = sys.stdout, sys.stderr
+ try:
+ sys.stdout, sys.stderr = repl.stdout, repl.stderr
+ yield
+ finally:
+ sys.stdout, sys.stderr = old_out, old_err
+
+class TestCurtsiesRewindRedraw(TestCurtsiesPainting):
+ def refresh(self, when='now'):
+ self.refresh_requests.append(RefreshRequestEvent(when=when))
+
+ def send_refreshes(self):
+ while self.refresh_requests:
+ self.repl.process_event(self.refresh_requests.pop())
+
+ def enter(self, line=None):
+ """Enter a line of text, avoiding autocompletion windows
+
+ autocomplete could still happen if the entered line has
+ autocompletion that would happen then, but intermediate
+ stages won't happen"""
+ if line is not None:
+ self.repl.current_line = line
+ with output_to_repl(self.repl):
+ self.repl.on_enter()
+ self.send_refreshes()
+
+ def undo(self):
+ with output_to_repl(self.repl):
+ self.repl.undo()
+ self.send_refreshes()
+
+ def setUp(self):
+ self.refresh_requests = []
+ self.repl = Repl(banner='', config=setup_config(), request_refresh=self.refresh)
+ self.repl.rl_history = History() # clear history
+ self.repl.height, self.repl.width = (5, 32)
+
+ def test_rewind(self):
+ self.repl.current_line = '1 + 1'
+ self.enter()
+ screen = [u'>>> 1 + 1',
+ u'2',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (2, 4))
+ self.repl.undo()
+ screen = [u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (0, 4))
+
+ @skip('wrong message')
+ def test_rewind_contiguity_loss(self):
+ self.enter('1 + 1')
+ self.enter('2 + 2')
+ self.enter('def foo(x):')
+ self.repl.current_line = ' return x + 1'
+ screen = [u'>>> 1 + 1',
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> def foo(x):',
+ u'... return x + 1']
+ self.assert_paint_ignoring_formatting(screen, (5, 8))
+ self.repl.scroll_offset = 1
+ self.assert_paint_ignoring_formatting(screen[1:], (4, 8))
+ self.undo()
+ screen = [u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (3, 4))
+ self.undo()
+ screen = [u'2',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (1, 4))
+ self.undo()
+ screen = [CONTIGUITY_BROKEN_MSG[:self.repl.width],
+ u'>>> ',
+ u'',
+ u'',
+ u'',
+ u' '] #TODO why is that there? Necessary?
+ self.assert_paint_ignoring_formatting(screen, (1, 4))
+ screen = [u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (0, 4))
+
+ def test_inconsistent_history_doesnt_happen_if_onscreen(self):
+ self.enter("1 + 1")
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (2, 4))
+ self.enter("2 + 2")
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (4, 4))
+ self.repl.display_lines[0] = self.repl.display_lines[0] * 2
+ self.undo()
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (2, 4))
+
+ @skip('why is everything moved up?')
+ def test_rewind_inconsistent_history(self):
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ self.enter("3 + 3")
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> 3 + 3',
+ u'6',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (6, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[2:], (4, 4))
+ self.repl.display_lines[0] = self.repl.display_lines[0] * 2
+ self.undo()
+ screen = [INCONSISTENT_HISTORY_MSG[:self.repl.width],
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ',
+ u'',
+ u'']
+ self.assert_paint_ignoring_formatting(screen, (5, 4))
+
From de56af5537aa74c527dc5ea4e416f8d811b1f2b2 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 14 Oct 2014 22:42:32 -0400
Subject: [PATCH 0073/1650] tests for clearing screen
---
bpython/test/test_curtsies_painting.py | 41 ++++++++++++++++++++++++++
1 file changed, 41 insertions(+)
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index e67d51cbf..6e903384f 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -207,3 +207,44 @@ def test_rewind_inconsistent_history(self):
u'']
self.assert_paint_ignoring_formatting(screen, (5, 4))
+ def test_clear_screen(self):
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (4, 4))
+ self.repl.request_paint_to_clear_screen = True
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ', u'', u'', u'', u'']
+ self.assert_paint_ignoring_formatting(screen, (4, 4))
+
+ @skip('the screen moved up again!')
+ def test_clear_screen_while_banner_visible(self):
+ self.repl.status_bar.message('STATUS_BAR')
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ',
+ u'STATUS_BAR ']
+ self.assert_paint_ignoring_formatting(screen, (4, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[1:], (3, 4))
+
+ self.repl.request_paint_to_clear_screen = True
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ',
+ u'', u'', u'',
+ u'STATUS_BAR ']
+ self.assert_paint_ignoring_formatting(screen, (0, 4))
From 9d487336bf9ba1186a1d61bc0605b23263235f47 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 14 Oct 2014 23:16:00 -0400
Subject: [PATCH 0074/1650] Remove curtsies fill_terminal option
At some point I thought character-perfect matching
of bpython.cli was important, but if folks want that UI, use cli!
---
bpython/config.py | 2 --
bpython/curtsiesfrontend/repl.py | 34 +++++----------------
doc/sphinx/source/configuration-options.rst | 7 -----
3 files changed, 7 insertions(+), 36 deletions(-)
diff --git a/bpython/config.py b/bpython/config.py
index 9aa01cf86..3de387c37 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -89,7 +89,6 @@ def loadini(struct, configfile):
},
'curtsies': {
'list_above' : False,
- 'fill_terminal' : False,
'right_arrow_completion' : True,
}})
if not config.read(config_path):
@@ -157,7 +156,6 @@ def loadini(struct, configfile):
struct.save_append_py = config.getboolean('general', 'save_append_py')
struct.curtsies_list_above = config.getboolean('curtsies', 'list_above')
- struct.curtsies_fill_terminal = config.getboolean('curtsies', 'fill_terminal')
struct.curtsies_right_arrow_completion = config.getboolean('curtsies', 'right_arrow_completion')
color_scheme_name = config.get('general', 'color_scheme')
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 7837316ba..5930f1f42 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -272,12 +272,7 @@ def smarter_request_reload(desc):
self.get_term_hw = get_term_hw
self.get_cursor_vertical_diff = get_cursor_vertical_diff
- self.status_bar = StatusBar(
- (_(" <%s> Rewind <%s> Save <%s> Pastebin <%s> Editor")
- % (config.undo_key, config.save_key, config.pastebin_key, config.external_editor_key)
- if config.curtsies_fill_terminal else ''),
- refresh_request=self.request_refresh
- )
+ self.status_bar = StatusBar('', refresh_request=self.request_refresh)
self.edit_keys = edit_keys.mapping_with_config(config, key_dispatch)
logger.debug("starting parent init")
super(Repl, self).__init__(interp, config)
@@ -1009,7 +1004,7 @@ def paint(self, about_to_exit=False, user_quit=False):
self.clean_up_current_line_for_exit() # exception to not changing state!
width, min_height = self.width, self.height
- show_status_bar = bool(self.status_bar.should_show_message) or (self.config.curtsies_fill_terminal or self.status_bar.has_focus)
+ show_status_bar = bool(self.status_bar.should_show_message) or self.status_bar.has_focus
if show_status_bar:
min_height -= 1
@@ -1017,10 +1012,7 @@ def paint(self, about_to_exit=False, user_quit=False):
#current_line_start_row = len(self.lines_for_display) - self.scroll_offset
if self.request_paint_to_clear_screen: # or show_status_bar and about_to_exit ?
self.request_paint_to_clear_screen = False
- if self.config.curtsies_fill_terminal: #TODO clean up this logic - really necessary check?
- arr = FSArray(self.height - 1 + current_line_start_row, width)
- else:
- arr = FSArray(self.height + current_line_start_row, width)
+ arr = FSArray(min_height + current_line_start_row, width)
else:
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
@@ -1106,23 +1098,11 @@ def paint(self, about_to_exit=False, user_quit=False):
logger.debug('about to exit: %r', about_to_exit)
if show_status_bar:
- if self.config.curtsies_fill_terminal:
- if about_to_exit:
- arr[max(arr.height, min_height), :] = FSArray(1, width)
- else:
- arr[max(arr.height, min_height), :] = paint.paint_statusbar(1, width, self.status_bar.current_line, self.config)
-
- if self.presentation_mode:
- rows = arr.height
- columns = arr.width
- last_key_box = paint.paint_last_events(rows, columns, [events.pp_event(x) for x in self.last_events if x])
- arr[arr.height-last_key_box.height:arr.height, arr.width-last_key_box.width:arr.width] = last_key_box
+ statusbar_row = min_height + 1 if arr.height == min_height else arr.height
+ if about_to_exit:
+ arr[statusbar_row, :] = FSArray(1, width)
else:
- statusbar_row = min_height + 1 if arr.height == min_height else arr.height
- if about_to_exit:
- arr[statusbar_row, :] = FSArray(1, width)
- else:
- arr[statusbar_row, :] = paint.paint_statusbar(1, width, self.status_bar.current_line, self.config)
+ arr[statusbar_row, :] = paint.paint_statusbar(1, width, self.status_bar.current_line, self.config)
if self.config.color_scheme['background'] not in ('d', 'D'):
for r in range(arr.height):
diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst
index 1aa79b4af..34b8d5da7 100644
--- a/doc/sphinx/source/configuration-options.rst
+++ b/doc/sphinx/source/configuration-options.rst
@@ -290,13 +290,6 @@ This refers to the ``[curtsies]`` section in your config file.
.. versionadded:: 0.13
-fill_terminal
-^^^^^^^^^^^^^
-Default: False
-
-Whether bpython should clear the screen on start, and always display a status
-bar at the bottom.
-
list_above
^^^^^^^^^^
Default: False
From 313189c017a66effe494f23b6a291d578ac5cdaa Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 14 Oct 2014 23:25:40 -0400
Subject: [PATCH 0075/1650] tests for clearing, fix issue #343
---
bpython/curtsiesfrontend/repl.py | 6 ++++--
bpython/test/test_curtsies_painting.py | 24 +++++++++++++++++-------
2 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 5930f1f42..df1075fab 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1006,9 +1006,11 @@ def paint(self, about_to_exit=False, user_quit=False):
width, min_height = self.width, self.height
show_status_bar = bool(self.status_bar.should_show_message) or self.status_bar.has_focus
if show_status_bar:
- min_height -= 1
+ min_height -= 1 # because we're going to tack the status bar on at the end,
+ # shoot for an array one less than the height of the screen
current_line_start_row = len(self.lines_for_display) - max(0, self.scroll_offset)
+ #TODO how is the situation of self.scroll_offset < 0 possible?
#current_line_start_row = len(self.lines_for_display) - self.scroll_offset
if self.request_paint_to_clear_screen: # or show_status_bar and about_to_exit ?
self.request_paint_to_clear_screen = False
@@ -1098,7 +1100,7 @@ def paint(self, about_to_exit=False, user_quit=False):
logger.debug('about to exit: %r', about_to_exit)
if show_status_bar:
- statusbar_row = min_height + 1 if arr.height == min_height else arr.height
+ statusbar_row = min_height if arr.height == min_height else arr.height
if about_to_exit:
arr[statusbar_row, :] = FSArray(1, width)
else:
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 6e903384f..a41ce2f17 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -71,7 +71,6 @@ def test_completion(self):
u'┌───────────────────────┐',
u'│ set( setattr( │',
u'└───────────────────────┘',
- u'',
u'Welcome to bpython! Press f']
self.assert_paint_ignoring_formatting(screen, (0, 4))
@@ -126,7 +125,6 @@ def test_rewind(self):
screen = [u'>>> ']
self.assert_paint_ignoring_formatting(screen, (0, 4))
- @skip('wrong message')
def test_rewind_contiguity_loss(self):
self.enter('1 + 1')
self.enter('2 + 2')
@@ -182,7 +180,7 @@ def test_inconsistent_history_doesnt_happen_if_onscreen(self):
u'>>> ']
self.assert_paint_ignoring_formatting(screen, (2, 4))
- @skip('why is everything moved up?')
+ @skip('for inconsistent history check')
def test_rewind_inconsistent_history(self):
self.enter("1 + 1")
self.enter("2 + 2")
@@ -224,8 +222,7 @@ def test_clear_screen(self):
u'>>> ', u'', u'', u'', u'']
self.assert_paint_ignoring_formatting(screen, (4, 4))
- @skip('the screen moved up again!')
- def test_clear_screen_while_banner_visible(self):
+ def test_scroll_down_while_banner_visible(self):
self.repl.status_bar.message('STATUS_BAR')
self.enter("1 + 1")
self.enter("2 + 2")
@@ -239,12 +236,25 @@ def test_clear_screen_while_banner_visible(self):
self.repl.scroll_offset += len(screen) - self.repl.height
self.assert_paint_ignoring_formatting(screen[1:], (3, 4))
- self.repl.request_paint_to_clear_screen = True
+ def test_clear_screen_while_banner_visible(self):
+ self.repl.status_bar.message('STATUS_BAR')
+ self.enter("1 + 1")
+ self.enter("2 + 2")
screen = [u">>> 1 + 1",
u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ',
+ u'STATUS_BAR ']
+ self.assert_paint_ignoring_formatting(screen, (4, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[1:], (3, 4))
+
+ self.repl.request_paint_to_clear_screen = True
+ screen = [u'2',
u'>>> 2 + 2',
u'4',
u'>>> ',
u'', u'', u'',
u'STATUS_BAR ']
- self.assert_paint_ignoring_formatting(screen, (0, 4))
+ self.assert_paint_ignoring_formatting(screen, (3, 4))
From 10948f8f8b19bf54c22e1b478d4052fcf78475f6 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Wed, 15 Oct 2014 08:52:01 -0400
Subject: [PATCH 0076/1650] test_rewind_contiguity_loss test passing
---
bpython/curtsiesfrontend/repl.py | 23 ++++++++++-------------
1 file changed, 10 insertions(+), 13 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index df1075fab..e3b6b8aef 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1018,7 +1018,7 @@ def paint(self, about_to_exit=False, user_quit=False):
else:
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
- if self.inconsistent_history == False & current_line_start_row >= 0:
+ if self.inconsistent_history == False and current_line_start_row >= 0:
logger.debug("start %i",current_line_start_row)
history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
arr[:history.height,:history.width] = history
@@ -1031,7 +1031,7 @@ def paint(self, about_to_exit=False, user_quit=False):
msg = INCONSISTENT_HISTORY_MSG
arr[0, 0:min(len(msg), width)] = [msg[:width]]
# self.scroll_offset -= 1
-
+
current_line_start_row = len(self.lines_for_display )- max(-1, self.scroll_offset)
self.inconsistent_history = False
if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
@@ -1244,19 +1244,16 @@ def reevaluate(self, insert_into_history=False):
self.process_event(events.RefreshRequestEvent())
sys.stdin = self.stdin
self.reevaluating = False
-
- num_lines_onscreen = len(self.lines_for_display) - max(0, self.scroll_offset)
- old_display_lines_offscreen = []
- display_lines_offscreen = []
- if old_display_lines[:len(self.display_lines) - num_lines_onscreen]!= self.display_lines:
- old_display_lines_offscreen = old_display_lines[:len(self.display_lines) - num_lines_onscreen]
- display_lines_offscreen = self.display_lines[:-num_lines_onscreen]
-
- if old_display_lines_offscreen != display_lines_offscreen and self.history_messed_up == False:
- self.scroll_offset=self.scroll_offset-(len(old_display_lines)-len(self.display_lines))
+ num_lines_onscreen = len(self.lines_for_display) - max(0, self.scroll_offset)
+ display_lines_offscreen = self.display_lines[:len(self.display_lines) - num_lines_onscreen]
+ old_display_lines_offscreen = old_display_lines[:len(self.display_lines) - num_lines_onscreen]
+ logger.debug('old_display_lines_offscreen %r', old_display_lines_offscreen)
+ logger.debug('display_lines_offscreen %r', display_lines_offscreen)
+ if old_display_lines_offscreen[:len(display_lines_offscreen)] != display_lines_offscreen and self.history_messed_up == False:
+ self.scroll_offset = self.scroll_offset - (len(old_display_lines)-len(self.display_lines))
self.inconsistent_history = True
- #self.scroll_offset = self.scroll_offset - max(-1,(len(old_display_lines_offscreen)-len(display_lines_offscreen)+1))
+ logger.debug('after rewind, self.inconsistent_history is %r', self.inconsistent_history)
self.cursor_offset = 0
self.current_line = ''
From 82b39bf4d887949fe88f328376011af4c195d50d Mon Sep 17 00:00:00 2001
From: lrraymond13
Date: Tue, 14 Oct 2014 18:48:43 -0400
Subject: [PATCH 0077/1650] Fixed #284
---
bpython/args.py | 2 ++
bpython/test/test_args.py | 20 ++++++++++++++++++++
2 files changed, 22 insertions(+)
create mode 100644 bpython/test/test_args.py
diff --git a/bpython/args.py b/bpython/args.py
index ff026357b..bf9f32603 100644
--- a/bpython/args.py
+++ b/bpython/args.py
@@ -90,6 +90,7 @@ def parse(args, extras=None, ignore_stdin=False):
if not ignore_stdin and not (sys.stdin.isatty() and sys.stdout.isatty()):
interpreter = code.InteractiveInterpreter()
+ print "Entering st.read %s" % sys.stdout.isatty()
interpreter.runsource(sys.stdin.read())
raise SystemExit
@@ -107,5 +108,6 @@ def exec_code(interpreter, args):
source = sourcefile.read()
old_argv, sys.argv = sys.argv, args
sys.path.insert(0, os.path.abspath(os.path.dirname(args[0])))
+ interpreter.locals['__file__'] = args[0]
interpreter.runsource(source, args[0], 'exec')
sys.argv = old_argv
diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py
new file mode 100644
index 000000000..51b1eb651
--- /dev/null
+++ b/bpython/test/test_args.py
@@ -0,0 +1,20 @@
+import unittest
+import subprocess
+import tempfile
+import sys
+
+
+
+
+class TestExecArgs(unittest.TestCase):
+ def test_exec_dunder_file(self):
+ with tempfile.NamedTemporaryFile(delete=False) as f:
+ f.write(
+ "import sys; print 'hello'; sys.stderr.write(__file__); sys.stderr.flush();")
+ f.flush()
+ print open(f.name).read()
+ p = subprocess.Popen(['bpython-curtsies', f.name], stderr= subprocess.PIPE)
+
+ self.assertEquals(p.stderr.read().strip(), f.name)
+
+
From 3fa21ab77927347587974cd312254bcc1cc33889 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 16 Oct 2014 11:29:11 -0400
Subject: [PATCH 0078/1650] fix Python 3 test
---
bpython/test/test_args.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py
index 51b1eb651..e14bf1afe 100644
--- a/bpython/test/test_args.py
+++ b/bpython/test/test_args.py
@@ -10,11 +10,11 @@ class TestExecArgs(unittest.TestCase):
def test_exec_dunder_file(self):
with tempfile.NamedTemporaryFile(delete=False) as f:
f.write(
- "import sys; print 'hello'; sys.stderr.write(__file__); sys.stderr.flush();")
+ "import sys; sys.stderr.write(__file__); sys.stderr.flush();".encode('ascii'))
f.flush()
print open(f.name).read()
- p = subprocess.Popen(['bpython-curtsies', f.name], stderr= subprocess.PIPE)
-
- self.assertEquals(p.stderr.read().strip(), f.name)
+ p = subprocess.Popen(['bpython-curtsies', f.name], stderr=subprocess.PIPE)
+
+ self.assertEquals(p.stderr.read().strip().decode('ascii'), f.name)
From 6469adf5b3d95c2dbdeda4c7b59af8d40e0aa785 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 16 Oct 2014 11:03:04 -0400
Subject: [PATCH 0079/1650] Tests for all sorts of inconsistent history edge
cases
Also fixes #371 and #375
bumps curtsies version for better testing functions
---
bpython/curtsiesfrontend/repl.py | 77 ++++++----
bpython/test/test_curtsies_painting.py | 198 ++++++++++++++++++++++++-
setup.py | 2 +-
3 files changed, 242 insertions(+), 35 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index e3b6b8aef..982796ae1 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -306,7 +306,7 @@ def smarter_request_reload(desc):
self.request_paint_to_clear_screen = False # next paint should clear screen
self.inconsistent_history = False # offscreen command yields different result from history
- self.history_messed_up = False # history error message displayed
+ self.history_already_messed_up = False # history error message displayed
self.last_events = [None] * 50 # some commands act differently based on the prev event
# this list doesn't include instances of event.Event,
# only keypress-type events (no refresh screen events etc.)
@@ -1018,31 +1018,27 @@ def paint(self, about_to_exit=False, user_quit=False):
else:
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
- if self.inconsistent_history == False and current_line_start_row >= 0:
- logger.debug("start %i",current_line_start_row)
- history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
- arr[:history.height,:history.width] = history
-
- else:
- if self.inconsistent_history == True and self.history_messed_up == False:
- self.history_messed_up = True
- #if INCONSISTENT_HISTORY_MSG not in self.display_lines:
- logger.debug(INCONSISTENT_HISTORY_MSG)
- msg = INCONSISTENT_HISTORY_MSG
- arr[0, 0:min(len(msg), width)] = [msg[:width]]
- # self.scroll_offset -= 1
-
- current_line_start_row = len(self.lines_for_display )- max(-1, self.scroll_offset)
- self.inconsistent_history = False
- if current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
- logger.debug(CONTIGUITY_BROKEN_MSG)
- msg = CONTIGUITY_BROKEN_MSG
- arr[0, 0:min(len(msg), width)] = [msg[:width]]
+ def move_screen_up(current_line_start_row):
# move screen back up a screen minus a line
- while current_line_start_row < 0:
- self.scroll_offset = self.scroll_offset - self.height
- current_line_start_row = len(self.lines_for_display) - max(-1, self.scroll_offset)
+ while current_line_start_row < 0:
+ logger.debug('scroll_offset was %s, current_line_start_row was %s', self.scroll_offset, current_line_start_row)
+ self.scroll_offset = self.scroll_offset - self.height
+ current_line_start_row = len(self.lines_for_display) - max(-1, self.scroll_offset)
+ logger.debug('scroll_offset changed to %s, current_line_start_row changed to %s', self.scroll_offset, current_line_start_row)
+ return current_line_start_row
+
+ if self.inconsistent_history == True and not self.history_already_messed_up:
+ logger.debug(INCONSISTENT_HISTORY_MSG)
+ self.history_already_messed_up = True
+ msg = INCONSISTENT_HISTORY_MSG
+ arr[0, 0:min(len(msg), width)] = [msg[:width]]
+ current_line_start_row += 1 # for the message
+ self.scroll_offset -= 1 # to make up for the scroll we're going to receive
+ # after we render scrolls down a line
+
+ current_line_start_row = move_screen_up(current_line_start_row)
+ logger.debug('current_line_start_row: %r', current_line_start_row)
history = paint.paint_history(max(0, current_line_start_row - 1), width, self.lines_for_display)
arr[1:history.height+1,:history.width] = history
@@ -1050,6 +1046,29 @@ def paint(self, about_to_exit=False, user_quit=False):
if arr.height <= min_height:
arr[min_height, 0] = ' ' # force scroll down to hide broken history message
+ elif current_line_start_row < 0: #if current line trying to be drawn off the top of the screen
+ logger.debug(CONTIGUITY_BROKEN_MSG)
+ msg = CONTIGUITY_BROKEN_MSG
+ arr[0, 0:min(len(msg), width)] = [msg[:width]]
+
+ current_line_start_row = move_screen_up(current_line_start_row)
+
+ history = paint.paint_history(max(0, current_line_start_row - 1), width, self.lines_for_display)
+ arr[1:history.height+1,:history.width] = history
+
+ if arr.height <= min_height:
+ arr[min_height, 0] = ' ' # force scroll down to hide broken history message
+
+ else:
+ assert current_line_start_row >= 0
+ logger.debug("no history issues. start %i",current_line_start_row)
+ history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
+ arr[:history.height,:history.width] = history
+
+
+
+
+ self.inconsistent_history = False
current_line = paint.paint_current_line(min_height, width, self.current_cursor_line)
if user_quit: # quit() or exit() in interp
@@ -1077,7 +1096,7 @@ def paint(self, about_to_exit=False, user_quit=False):
assert cursor_column >= 0, (cursor_column, len(self.current_cursor_line), len(self.current_line), self.cursor_offset)
cursor_row += current_line_start_row
- if self.list_win_visible:
+ if self.list_win_visible and not self.coderunner.running:
logger.debug('infobox display code running')
visible_space_above = history.height
visible_space_below = min_height - current_line_end_row - 1
@@ -1248,10 +1267,10 @@ def reevaluate(self, insert_into_history=False):
num_lines_onscreen = len(self.lines_for_display) - max(0, self.scroll_offset)
display_lines_offscreen = self.display_lines[:len(self.display_lines) - num_lines_onscreen]
old_display_lines_offscreen = old_display_lines[:len(self.display_lines) - num_lines_onscreen]
- logger.debug('old_display_lines_offscreen %r', old_display_lines_offscreen)
- logger.debug('display_lines_offscreen %r', display_lines_offscreen)
- if old_display_lines_offscreen[:len(display_lines_offscreen)] != display_lines_offscreen and self.history_messed_up == False:
- self.scroll_offset = self.scroll_offset - (len(old_display_lines)-len(self.display_lines))
+ logger.debug('old_display_lines_offscreen %s', '|'.join([str(x) for x in old_display_lines_offscreen]))
+ logger.debug(' display_lines_offscreen %s', '|'.join([str(x) for x in display_lines_offscreen]))
+ if old_display_lines_offscreen[:len(display_lines_offscreen)] != display_lines_offscreen and not self.history_already_messed_up:
+ #self.scroll_offset = self.scroll_offset + (len(old_display_lines)-len(self.display_lines))
self.inconsistent_history = True
logger.debug('after rewind, self.inconsistent_history is %r', self.inconsistent_history)
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index a41ce2f17..6013a6ce8 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -35,10 +35,11 @@ def assert_paint(self, screen, cursor_row_col):
self.assertFSArraysEqual(array, screen)
self.assertEqual(cursor_pos, cursor_row_col)
- def assert_paint_ignoring_formatting(self, screen, cursor_row_col):
+ def assert_paint_ignoring_formatting(self, screen, cursor_row_col=None):
array, cursor_pos = self.repl.paint()
self.assertFSArraysEqualIgnoringFormatting(array, screen)
- self.assertEqual(cursor_pos, cursor_row_col)
+ if cursor_row_col is not None:
+ self.assertEqual(cursor_pos, cursor_row_col)
class TestCurtsiesPaintingSimple(TestCurtsiesPainting):
@@ -180,7 +181,6 @@ def test_inconsistent_history_doesnt_happen_if_onscreen(self):
u'>>> ']
self.assert_paint_ignoring_formatting(screen, (2, 4))
- @skip('for inconsistent history check')
def test_rewind_inconsistent_history(self):
self.enter("1 + 1")
self.enter("2 + 2")
@@ -202,8 +202,172 @@ def test_rewind_inconsistent_history(self):
u'4',
u'>>> ',
u'',
- u'']
- self.assert_paint_ignoring_formatting(screen, (5, 4))
+ u' ']
+ self.assert_paint_ignoring_formatting(screen, (3, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[1:-2], (2, 4))
+ self.assert_paint_ignoring_formatting(screen[1:-2], (2, 4))
+
+ def test_rewind_inconsistent_history_more_lines_same_screen(self):
+ self.repl.width = 60
+ sys.a = 5
+ self.enter("import sys")
+ self.enter("for i in range(sys.a): print(sys.a)")
+ self.enter()
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ screen = [u">>> import sys",
+ u">>> for i in range(sys.a): print(sys.a)",
+ u'... ',
+ u'5',
+ u'5',
+ u'5',
+ u'5',
+ u'5',
+ u'>>> 1 + 1',
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (12, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[8:], (4, 4))
+ sys.a = 6
+ self.undo()
+ screen = [INCONSISTENT_HISTORY_MSG[:self.repl.width],
+ u'6',
+ u'>>> 1 + 1', # everything will jump down a line - that's perfectly reasonable
+ u'2',
+ u'>>> ',
+ u' ']
+ self.assert_paint_ignoring_formatting(screen, (4, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[1:-1], (3, 4))
+
+ def test_rewind_inconsistent_history_more_lines_lower_screen(self):
+ self.repl.width = 60
+ sys.a = 5
+ self.enter("import sys")
+ self.enter("for i in range(sys.a): print(sys.a)")
+ self.enter()
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ screen = [u">>> import sys",
+ u">>> for i in range(sys.a): print(sys.a)",
+ u'... ',
+ u'5',
+ u'5',
+ u'5',
+ u'5',
+ u'5',
+ u'>>> 1 + 1',
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (12, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[8:], (4, 4))
+ sys.a = 8
+ self.undo()
+ screen = [INCONSISTENT_HISTORY_MSG[:self.repl.width],
+ u'8',
+ u'8',
+ u'8',
+ u'>>> 1 + 1',
+ u'2',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen)
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[-5:])
+
+ def test_rewind_inconsistent_history_more_lines_raise_screen(self):
+ self.repl.width = 60
+ sys.a = 5
+ self.enter("import sys")
+ self.enter("for i in range(sys.a): print(sys.a)")
+ self.enter()
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ screen = [u">>> import sys",
+ u">>> for i in range(sys.a): print(sys.a)",
+ u'... ',
+ u'5',
+ u'5',
+ u'5',
+ u'5',
+ u'5',
+ u'>>> 1 + 1',
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (12, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[8:], (4, 4))
+ sys.a = 1
+ self.undo()
+ screen = [INCONSISTENT_HISTORY_MSG[:self.repl.width],
+ u'1',
+ u'>>> 1 + 1',
+ u'2',
+ u'>>> ',
+ u' ']
+ self.assert_paint_ignoring_formatting(screen)
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[1:-1])
+
+ def test_rewind_history_not_quite_inconsistent(self):
+ self.repl.width = 50
+ sys.a = 5
+ self.enter("for i in range(__import__('sys').a): print(i)")
+ self.enter()
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ screen = [u">>> for i in range(__import__('sys').a): print(i)",
+ u'... ',
+ u'0',
+ u'1',
+ u'2',
+ u'3',
+ u'4',
+ u'>>> 1 + 1',
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (11, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[7:], (4, 4))
+ sys.a = 6
+ self.undo()
+ screen = [u'5',
+ u'>>> 1 + 1', # everything will jump down a line - that's perfectly reasonable
+ u'2',
+ u'>>> ',]
+ self.assert_paint_ignoring_formatting(screen, (3, 4))
+
+ def test_rewind_barely_consistent(self):
+ self.enter("1 + 1")
+ self.enter("2 + 2")
+ self.enter("3 + 3")
+ screen = [u">>> 1 + 1",
+ u'2',
+ u'>>> 2 + 2',
+ u'4',
+ u'>>> 3 + 3',
+ u'6',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (6, 4))
+ self.repl.scroll_offset += len(screen) - self.repl.height
+ self.assert_paint_ignoring_formatting(screen[2:], (4, 4))
+ self.repl.display_lines[2] = self.repl.display_lines[2] * 2
+ self.undo()
+ screen = [u'>>> 2 + 2',
+ u'4',
+ u'>>> ']
+ self.assert_paint_ignoring_formatting(screen, (2, 4))
+
def test_clear_screen(self):
self.enter("1 + 1")
@@ -258,3 +422,27 @@ def test_clear_screen_while_banner_visible(self):
u'', u'', u'',
u'STATUS_BAR ']
self.assert_paint_ignoring_formatting(screen, (3, 4))
+
+ def test_cursor_stays_at_bottom_of_screen(self):
+ """infobox showing up during intermediate render was causing this to fail, #371"""
+ self.repl.width = 50
+ self.repl.current_line = "__import__('random').__name__"
+ with output_to_repl(self.repl):
+ self.repl.on_enter()
+ screen = [u">>> __import__('random').__name__",
+ u"'random'"]
+ self.assert_paint_ignoring_formatting(screen)
+
+ with output_to_repl(self.repl):
+ self.repl.process_event(self.refresh_requests.pop())
+ screen = [u">>> __import__('random').__name__",
+ u"'random'",
+ u""]
+ self.assert_paint_ignoring_formatting(screen)
+
+ with output_to_repl(self.repl):
+ self.repl.process_event(self.refresh_requests.pop())
+ screen = [u">>> __import__('random').__name__",
+ u"'random'",
+ u">>> "]
+ self.assert_paint_ignoring_formatting(screen, (2, 4))
diff --git a/setup.py b/setup.py
index 8de734e8c..f43f589dd 100755
--- a/setup.py
+++ b/setup.py
@@ -151,7 +151,7 @@ def initialize_options(self):
if sys.version_info[:2] >= (2, 6):
# curtsies only supports 2.6 and onwards
- extras_require['curtsies'] = ['curtsies >=0.1.7, <0.2.0', 'greenlet']
+ extras_require['curtsies'] = ['curtsies >=0.1.11, <0.2.0', 'greenlet']
extras_require['watch'] = ['watchdog']
packages.append("bpython.curtsiesfrontend")
entry_points['console_scripts'].append(
From 4ae4c81ebaab8007236a868801cdd1d1899c89b1 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 16 Oct 2014 23:16:42 -0400
Subject: [PATCH 0080/1650] first pass at python version banner
---
bpython/args.py | 4 ++++
bpython/curtsies.py | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/bpython/args.py b/bpython/args.py
index bf9f32603..8f8e8a5be 100644
--- a/bpython/args.py
+++ b/bpython/args.py
@@ -22,6 +22,10 @@ def error(self, msg):
raise OptionParserFailed()
+def version_banner():
+ return 'bpython version %s on top of Python %s %s' % (
+ __version__, sys.version.split()[0], sys.executable)
+
def parse(args, extras=None, ignore_stdin=False):
"""Receive an argument list - if None, use sys.argv - parse all args and
take appropriate action. Also receive optional extra options: this should
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 0657c0115..4e6bf3e17 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -61,7 +61,7 @@ def main(args=None, locals_=None, banner=None):
else:
sys.path.insert(0, '') # expected for interactive sessions (vanilla python does it)
-
+ print(bpargs.version_banner())
mainloop(config, locals_, banner, interp, paste, interactive=(not exec_args))
def mainloop(config, locals_, banner, interp=None, paste=None, interactive=True):
From 8321deff122819a52bcb14cba4749665ba6cb349 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 16 Oct 2014 23:32:46 -0400
Subject: [PATCH 0081/1650] remove nop test in test_manual_readline
---
bpython/test/test_manual_readline.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index 8bb6eb0da..5cfdcc6c7 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -258,11 +258,6 @@ class config: att = 'c'
self.assertEqual(configured_edits.call('c', cursor_offset=5, line='asfd'),
('hi', 2))
- def test_actual(self):
- class config: att = 'c'
- key_dispatch = {'c': 'c'}
- configured_edits = self.edits.mapping_with_config(config, key_dispatch)
-
if __name__ == '__main__':
unittest.main()
From 663ea479c1bca96ec0d806246c794ff138153258 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Fri, 17 Oct 2014 21:09:11 +0200
Subject: [PATCH 0082/1650] Remove debug prints.
---
bpython/args.py | 1 -
bpython/test/test_args.py | 1 -
2 files changed, 2 deletions(-)
diff --git a/bpython/args.py b/bpython/args.py
index 8f8e8a5be..0e97985ad 100644
--- a/bpython/args.py
+++ b/bpython/args.py
@@ -94,7 +94,6 @@ def parse(args, extras=None, ignore_stdin=False):
if not ignore_stdin and not (sys.stdin.isatty() and sys.stdout.isatty()):
interpreter = code.InteractiveInterpreter()
- print "Entering st.read %s" % sys.stdout.isatty()
interpreter.runsource(sys.stdin.read())
raise SystemExit
diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py
index e14bf1afe..829081837 100644
--- a/bpython/test/test_args.py
+++ b/bpython/test/test_args.py
@@ -12,7 +12,6 @@ def test_exec_dunder_file(self):
f.write(
"import sys; sys.stderr.write(__file__); sys.stderr.flush();".encode('ascii'))
f.flush()
- print open(f.name).read()
p = subprocess.Popen(['bpython-curtsies', f.name], stderr=subprocess.PIPE)
self.assertEquals(p.stderr.read().strip().decode('ascii'), f.name)
From d7afe7350654f609f344d4dd0f341a1ae0791118 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Fri, 17 Oct 2014 21:32:24 +0200
Subject: [PATCH 0083/1650] Small (subjective) clean-up in tests.
---
bpython/test/test_args.py | 23 +++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py
index 829081837..2c876847a 100644
--- a/bpython/test/test_args.py
+++ b/bpython/test/test_args.py
@@ -1,19 +1,22 @@
import unittest
import subprocess
-import tempfile
import sys
-
-
+import tempfile
+from textwrap import dedent
class TestExecArgs(unittest.TestCase):
def test_exec_dunder_file(self):
- with tempfile.NamedTemporaryFile(delete=False) as f:
- f.write(
- "import sys; sys.stderr.write(__file__); sys.stderr.flush();".encode('ascii'))
+ with tempfile.NamedTemporaryFile(delete=False, mode="w") as f:
+ f.write(dedent("""\
+ import sys
+ sys.stderr.write(__file__)
+ sys.stderr.flush()"""))
f.flush()
- p = subprocess.Popen(['bpython-curtsies', f.name], stderr=subprocess.PIPE)
-
- self.assertEquals(p.stderr.read().strip().decode('ascii'), f.name)
-
+ p = subprocess.Popen(
+ [sys.executable, "-m", "bpython.curtsies", f.name],
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ (_, stderr) = p.communicate()
+ self.assertEquals(stderr.strip(), f.name)
From 3995c1261c11bd137f526c3f53ada59f5094a8a1 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 17 Oct 2014 12:17:17 -0400
Subject: [PATCH 0084/1650] add failing test for issue #415
---
bpython/test/test_curtsies_repl.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index a83200607..f72bb2f67 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -1,3 +1,4 @@
+# coding: utf8
import code
import os
import sys
@@ -51,6 +52,10 @@ def test_external_communication(self):
self.repl.send_current_block_to_external_editor()
self.repl.send_session_to_external_editor()
+ def test_external_communication_encoding(self):
+ self.repl.display_lines.append(u'>>> åß∂ƒ')
+ self.repl.send_session_to_external_editor()
+
def test_get_last_word(self):
self.repl.rl_history.entries=['1','2 3','4 5 6']
self.repl._set_current_line('abcde')
From 3cddcc61fce73c85b4b7141ca858b3ad5b8f714b Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 17 Oct 2014 19:54:00 -0400
Subject: [PATCH 0085/1650] fix 415
---
bpython/curtsiesfrontend/interpreter.py | 9 ++++-----
bpython/curtsiesfrontend/repl.py | 12 ++++++------
bpython/repl.py | 7 +++++--
bpython/test/test_curtsies_repl.py | 5 +++--
4 files changed, 18 insertions(+), 15 deletions(-)
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index 3be4a25f2..a030ea12a 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -36,16 +36,16 @@ class BPythonFormatter(Formatter):
them into the appropriate format string
as defined above, then writes to the outfile
object the final formatted string. This does not write real strings. It writes format string (FmtStr) objects.
-
+
See the Pygments source for more info; it's pretty
straightforward."""
-
+
def __init__(self, color_scheme, **options):
self.f_strings = {}
for k, v in color_scheme.iteritems():
self.f_strings[k] = '\x01%s' % (v,)
Formatter.__init__(self, **options)
-
+
def format(self, tokensource, outfile):
o = ''
@@ -54,7 +54,7 @@ def format(self, tokensource, outfile):
token = token.parent
o += "%s\x03%s\x04" % (self.f_strings[token], text)
outfile.write(parse(o.rstrip()))
-
+
class Interp(code.InteractiveInterpreter):
def __init__(self, locals=None):
"""Constructor.
@@ -128,7 +128,6 @@ def showtraceback(self):
lexer = get_lexer_by_name("pytb", stripall=True)
self.format(tbtext,lexer)
-
def format(self, tbtext, lexer):
traceback_informative_formatter = BPythonFormatter(default_colors)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 2b5854055..54b89bca8 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -667,7 +667,7 @@ def process_simple_keypress(self, e):
self.add_normal_character(e)
def send_current_block_to_external_editor(self, filename=None):
- text = self.send_to_external_editor(self.get_current_block().encode('utf8'))
+ text = self.send_to_external_editor(self.get_current_block())
lines = [line for line in text.split('\n')]
while lines and not lines[-1].split():
lines.pop()
@@ -679,11 +679,11 @@ def send_current_block_to_external_editor(self, filename=None):
self.cursor_offset = len(self.current_line)
def send_session_to_external_editor(self, filename=None):
- for_editor = '### current bpython session - file will be reevaluated, ### lines will not be run\n'.encode('utf8')
- for_editor += ('\n'.join(line[len(self.ps1):] if line.startswith(self.ps1) else
- (line[len(self.ps2):] if line.startswith(self.ps2) else
- '### '+line)
- for line in self.getstdout().split('\n')).encode('utf8'))
+ for_editor = u'### current bpython session - file will be reevaluated, ### lines will not be run\n'
+ for_editor += '\n'.join(line[len(self.ps1):] if line.startswith(self.ps1) else
+ (line[len(self.ps2):] if line.startswith(self.ps2) else
+ '### '+line)
+ for line in self.getstdout().split('\n'))
text = self.send_to_external_editor(for_editor)
lines = text.split('\n')
self.history = [line for line in lines if line[:4] != '### ']
diff --git a/bpython/repl.py b/bpython/repl.py
index 6abc07ec3..13c06fb5c 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -1046,11 +1046,14 @@ def send_to_external_editor(self, text, filename=None):
"""Returns modified text from an editor, or the oriignal text if editor exited with non-zero"""
editor_args = shlex.split(self.config.editor)
with tempfile.NamedTemporaryFile(suffix='.py') as temp:
- temp.write(text)
+ temp.write(text.encode(getpreferredencoding()))
temp.flush()
if subprocess.call(editor_args + [temp.name]) == 0:
with open(temp.name) as f:
- return f.read()
+ if py3:
+ return f.read()
+ else:
+ return f.read().decode(getpreferredencoding())
else:
return text
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index f72bb2f67..00652fa78 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -53,8 +53,9 @@ def test_external_communication(self):
self.repl.send_session_to_external_editor()
def test_external_communication_encoding(self):
- self.repl.display_lines.append(u'>>> åß∂ƒ')
- self.repl.send_session_to_external_editor()
+ with captured_output():
+ self.repl.display_lines.append(u'>>> "åß∂ƒ"')
+ self.repl.send_session_to_external_editor()
def test_get_last_word(self):
self.repl.rl_history.entries=['1','2 3','4 5 6']
From 970d3b9298295bf0218467e507675b9bf0037709 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 17 Oct 2014 20:11:53 -0400
Subject: [PATCH 0086/1650] just do module discovery once and skip urwid
UrwidCrashersTest
---
bpython/test/test_crashers.py | 7 +++++++
bpython/test/test_importcompletion.py | 6 ++++--
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py
index 1b85e6916..533378ca1 100644
--- a/bpython/test/test_crashers.py
+++ b/bpython/test/test_crashers.py
@@ -7,6 +7,12 @@
import textwrap
import unittest
+try:
+ from unittest import skip
+except ImportError:
+ def skip(f):
+ return lambda self: None
+
try:
from twisted.internet import reactor
from twisted.internet.defer import Deferred
@@ -104,6 +110,7 @@ def check_no_traceback(self, data):
class CursesCrashersTest(TrialTestCase, CrashersTest):
backend = "cli"
+ @skip("take 6 seconds, and Simon says we can skip them")
class UrwidCrashersTest(TrialTestCase, CrashersTest):
backend = "urwid"
diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py
index 5124455eb..706be7bdb 100644
--- a/bpython/test/test_importcompletion.py
+++ b/bpython/test/test_importcompletion.py
@@ -15,11 +15,13 @@ def test_package_completion(self):
class TestRealComplete(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
[_ for _ in importcompletion.find_iterator]
__import__('sys')
__import__('os')
- def tearDown(self):
+ @classmethod
+ def tearDownClass(cls):
importcompletion.find_iterator = importcompletion.find_all_modules()
importcompletion.modules = set()
def test_from_attribute(self):
From 8fbb11e1ec33a04d42ed11920d164f386982a47f Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 17 Oct 2014 20:34:00 -0400
Subject: [PATCH 0087/1650] convert tabs in rl_history entries to spaces
This will lead to duplicate history entries because the strings
will no longer match, but it's an improvement
---
bpython/curtsiesfrontend/repl.py | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 54b89bca8..fc37cf56f 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -643,14 +643,16 @@ def yank_from_buffer(self):
def up_one_line(self):
self.rl_history.enter(self.current_line)
- self._set_current_line(self.rl_history.back(False, search=self.config.curtsies_right_arrow_completion),
- reset_rl_history=False)
+ self._set_current_line(tabs_to_spaces(self.rl_history.back(False,
+ search=self.config.curtsies_right_arrow_completion)),
+ reset_rl_history=False)
self._set_cursor_offset(len(self.current_line), reset_rl_history=False)
def down_one_line(self):
self.rl_history.enter(self.current_line)
- self._set_current_line(self.rl_history.forward(False, search=self.config.curtsies_right_arrow_completion),
- reset_rl_history=False)
+ self._set_current_line(tabs_to_spaces(self.rl_history.forward(False,
+ search=self.config.curtsies_right_arrow_completion)),
+ reset_rl_history=False)
self._set_cursor_offset(len(self.current_line), reset_rl_history=False)
def process_simple_keypress(self, e):
@@ -1369,6 +1371,9 @@ def key_help_text(self):
def is_nop(char):
return unicodedata.category(unicode(char)) == 'Cc'
+def tabs_to_spaces(line):
+ return line.replace('\t', ' ')
+
def compress_paste_event(paste_event):
"""If all events in a paste event are identical and not simple characters, returns one of them
From 9a90d60dd680c54f508dfea1af24e3247d164a9c Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 17 Oct 2014 23:16:54 -0400
Subject: [PATCH 0088/1650] Improve fish-style highlighted reverse search
---
bpython/curtsiesfrontend/repl.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index fc37cf56f..785f3d149 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -927,7 +927,7 @@ def current_line_formatted(self):
if self.incremental_search_target in self.current_line:
fs = fmtfuncs.on_magenta(self.incremental_search_target).join(fs.split(self.incremental_search_target))
elif self.rl_history.saved_line and self.rl_history.saved_line in self.current_line:
- if self.config.curtsies_right_arrow_completion:
+ if self.config.curtsies_right_arrow_completion and self.rl_history.index != 0:
fs = fmtfuncs.on_magenta(self.rl_history.saved_line).join(fs.split(self.rl_history.saved_line))
logger.debug('Display line %r -> %r', self.current_line, fs)
else:
@@ -1203,6 +1203,8 @@ def __repr__(self):
def _get_current_line(self):
return self._current_line
def _set_current_line(self, line, update_completion=True, reset_rl_history=True, clear_special_mode=True):
+ if self._current_line == line:
+ return
self._current_line = line
if update_completion:
self.update_completion()
@@ -1214,7 +1216,7 @@ def _set_current_line(self, line, update_completion=True, reset_rl_history=True,
"The current line")
def _get_cursor_offset(self):
return self._cursor_offset
- def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=True, clear_special_mode=True):
+ def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=False, clear_special_mode=True):
if update_completion:
self.update_completion()
if reset_rl_history:
From e31a8b5dd9eccc18f8941ea153ceb04505594446 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 00:07:08 -0400
Subject: [PATCH 0089/1650] Added most options to config
left out ones I'm thinking might be deprecated or
don't apply to bpython-curtsies
---
bpython/sample-config | 57 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 48 insertions(+), 9 deletions(-)
diff --git a/bpython/sample-config b/bpython/sample-config
index bcb2c0901..e75378649 100644
--- a/bpython/sample-config
+++ b/bpython/sample-config
@@ -15,29 +15,68 @@
auto_display_list = True
# Syntax highlighting as you type (default: True).
-syntax = True
+# syntax = True
# Display the arg spec (list of arguments) for callables,
# when possible (default: True).
-arg_spec = True
+# arg_spec = True
# History file (default: ~/.pythonhist):
-hist_file = ~/.pythonhist
+# hist_file = ~/.pythonhist
# Number of lines to store in history (set to 0 to disable) (default: 100):
-hist_length = 100
+# hist_length = 100
# Soft tab size (default: 4, see pep-8):
-tab_length = 4
+# tab_length = 4
# Color schemes should be put in $XDG_CONFIG_HOME/bpython/ e.g. to use the theme
# $XDG_CONFIG_HOME/bpython/foo.theme set color_scheme = foo. Leave blank or set
# to "default" to use the default theme
-color_scheme = default
+# color_scheme = default
# External editor to use for editing the current line, block, or full history
-editor = vi
+# Default is to try $EDITOR and $VISUAL, then vi - but if you uncomment
+# the line below that will take precedence
+# editor = vi
+
+# Whether to append .py to the filename while saving session to a file.
+# (default: False)
+# save_append_py = False
+
+# The name of a helper executable that should perform pastebin upload on bpython’s behalf.
+#pastebin_helper = gist.py
[keyboard]
-pastebin = F8
-save = C-s
+
+# pastebin = F8
+# last_output = F9
+# reimport = F6
+# help = F1
+# toggle_file_watch = F5
+# save = C-s
+# undo = C-r
+# up_one_line = C-p
+# down_one_line = C-n
+# cut_to_buffer = C-k
+# search = C-o
+# yank_from_buffer = C-y
+# clear_word = C-w
+# clear_line = C-u
+# clear_screen = C-l
+# show_source = F2
+# exit = C-d
+# external_editor = F7
+# edit_config = F3
+#
+[curtsies]
+
+# Allow the the completion and docstring box above the current line
+# (default: False)
+# list_above = False
+
+# Enables two fish (the shell) style features:
+# Previous line key will search for the current line (like reverse incremental
+# search) and right arrow will complete the current line with the first match
+# from history. (default: True)
+# right_arrow_completion = True
From 9d8a6aaeb674fb213de596447d438851159f5294 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 00:20:10 -0400
Subject: [PATCH 0090/1650] remove sample-config from setup.py
now it's in the bpython package for easier programmatic access
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index f7bc9b092..685894e90 100755
--- a/setup.py
+++ b/setup.py
@@ -187,7 +187,7 @@ def initialize_options(self):
packages = packages,
data_files = data_files,
package_data = {
- 'bpython': ['logo.png', 'sample-config'],
+ 'bpython': ['logo.png'],
'bpython.translations': mo_files,
'bpython.test': ['test.config', 'test.theme']
},
From 20f0b3b0add029daeece8061955c38f53765bcb0 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 01:39:58 -0400
Subject: [PATCH 0091/1650] remove outdated testing beeper module
---
bpython/curtsiesfrontend/beeper.py | 9 ---------
1 file changed, 9 deletions(-)
delete mode 100644 bpython/curtsiesfrontend/beeper.py
diff --git a/bpython/curtsiesfrontend/beeper.py b/bpython/curtsiesfrontend/beeper.py
deleted file mode 100644
index 287013c09..000000000
--- a/bpython/curtsiesfrontend/beeper.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import time
-import sys
-
-if __name__ == '__main__':
- while True:
- sys.stdout.write('beep\n')
- sys.stdout.flush()
- time.sleep(5)
-
From 9fa5e7e4e23e90e6d6cebf7a74c0ca67e955f8d7 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 01:49:26 -0400
Subject: [PATCH 0092/1650] fix hacky __import__ override
fixes #418
---
bpython/curtsiesfrontend/repl.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 785f3d149..45ba07e7d 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -339,11 +339,11 @@ def __enter__(self):
self.orig_import = __builtins__['__import__']
if self.watcher:
- old_module_locations = {} # for readding modules if they fail to load
+ old_module_locations = {} # for reading modules if they fail to load
@functools.wraps(self.orig_import)
def new_import(name, globals={}, locals={}, fromlist=[], level=-1):
try:
- m = self.orig_import(name, globals=globals, locals=locals, fromlist=fromlist)
+ m = self.orig_import(name, globals=globals, locals=locals, fromlist=fromlist, level=level)
except:
if name in old_module_locations:
loc = old_module_locations[name]
From 4a43c116630a56a0df875b819a735928141e8028 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 02:17:40 -0400
Subject: [PATCH 0093/1650] first pass at suspending
Doesn't put the cursor back very well at all, but since we're
going to have no idea what has happened while Python
was suspended there's probably not much we can do.
---
bpython/curtsies.py | 12 +++++++++++-
bpython/curtsiesfrontend/repl.py | 16 +++++++++++++++-
2 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 4e6bf3e17..431fe4f09 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -99,6 +99,14 @@ def event_or_refresh(timeout=None):
if starttime + timeout < time.time() or e is not None:
yield e
+ def on_suspend():
+ window.__exit__(None, None, None)
+ input_generator.__exit__(None, None, None)
+
+ def after_suspend():
+ input_generator.__enter__()
+ window.__enter__()
+
global repl # global for easy introspection `from bpython.curtsies import repl`
with Repl(config=config,
locals_=locals_,
@@ -109,7 +117,9 @@ def event_or_refresh(timeout=None):
banner=banner,
interp=interp,
interactive=interactive,
- orig_tcattrs=input_generator.original_stty) as repl:
+ orig_tcattrs=input_generator.original_stty,
+ on_suspend=on_suspend,
+ after_suspend=after_suspend) as repl:
repl.height, repl.width = window.t.height, window.t.width
def process_event(e):
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 45ba07e7d..2a2e5c871 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -219,7 +219,9 @@ def __init__(self,
banner=None,
interp=None,
interactive=True,
- orig_tcattrs=None):
+ orig_tcattrs=None,
+ on_suspend=lambda *args: None,
+ after_suspend=lambda *args: None):
"""
locals_ is a mapping of locals to pass into the interpreter
config is a bpython config.Struct with config attributes
@@ -298,6 +300,8 @@ def smarter_request_reload(desc):
# because there wasn't room to display everything
self._cursor_offset = 0 # from the left, 0 means first char
self.orig_tcattrs = orig_tcattrs # useful for shelling out with normal terminal
+ self.on_suspend = on_suspend
+ self.after_suspend = after_suspend
self.coderunner = CodeRunner(self.interp, self.request_refresh)
self.stdout = FakeOutput(self.coderunner, self.send_to_stdout)
@@ -335,7 +339,9 @@ def __enter__(self):
sys.stderr = self.stderr
sys.stdin = self.stdin
self.orig_sigwinch_handler = signal.getsignal(signal.SIGWINCH)
+ self.orig_sigtstp_handler = signal.getsignal(signal.SIGTSTP)
signal.signal(signal.SIGWINCH, self.sigwinch_handler)
+ signal.signal(signal.SIGTSTP, self.sigtstp_handler)
self.orig_import = __builtins__['__import__']
if self.watcher:
@@ -369,6 +375,7 @@ def __exit__(self, *args):
sys.stdout = self.orig_stdout
sys.stderr = self.orig_stderr
signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler)
+ signal.signal(signal.SIGTSTP, self.orig_sigtstp_handler)
__builtins__['__import__'] = self.orig_import
def sigwinch_handler(self, signum, frame):
@@ -379,6 +386,13 @@ def sigwinch_handler(self, signum, frame):
logger.info('sigwinch! Changed from %r to %r', (old_rows, old_columns), (self.height, self.width))
logger.info('decreasing scroll offset by %d to %d', cursor_dy, self.scroll_offset)
+ def sigtstp_handler(self, signum, frame):
+ self.__exit__()
+ self.on_suspend()
+ os.kill(os.getpid(), signal.SIGTSTP)
+ self.after_suspend()
+ self.__enter__()
+
def startup(self):
"""
Execute PYTHONSTARTUP file if it exits. Call this after front
From 06b9400565bfb34c6c191b2c930048af5ccde512 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 02:53:53 -0400
Subject: [PATCH 0094/1650] formatting and tests
---
bpython/curtsiesfrontend/repl.py | 14 ++++++--------
bpython/test/test_curtsies_painting.py | 5 +++++
2 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 2a2e5c871..ace8a66fc 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -547,17 +547,15 @@ def process_key_event(self, e):
def get_last_word(self):
def last_word(line):
- if not line:
- return ''
- return line.split().pop()
+ return line.split().pop() if line else ''
previous_word = last_word(self.rl_history.entry)
word = last_word(self.rl_history.back())
- line=self.current_line
- self._set_current_line(line[:len(line)-len(previous_word)]+word, reset_rl_history=False)
-
- self._set_cursor_offset(self.cursor_offset-len(previous_word)+len(word), reset_rl_history=False)
-
+ line = self.current_line
+ self._set_current_line(line[:len(line)-len(previous_word)] + word,
+ reset_rl_history=False)
+ self._set_cursor_offset(self.cursor_offset-len(previous_word) + len(word),
+ reset_rl_history=False)
def incremental_search(self, reverse=False, include_current=False):
if self.special_mode == None:
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index ce034a1db..098a9d31f 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -40,6 +40,11 @@ def assert_paint_ignoring_formatting(self, screen, cursor_row_col=None):
if cursor_row_col is not None:
self.assertEqual(cursor_pos, cursor_row_col)
+class TestCurtsiesPaintingTest(TestCurtsiesPainting):
+
+ def test_history_is_cleared(self):
+ self.assertEqual(self.repl.rl_history.entries, [''])
+
class TestCurtsiesPaintingSimple(TestCurtsiesPainting):
def test_startup(self):
From e854572cffe5f67e4f53d294c9155ffa75882562 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 12:02:01 -0400
Subject: [PATCH 0095/1650] Automatic refresh only every 1 second
Going to phase out the scheduled refreshes, and this will make bugs
more obvious in the meantime
---
bpython/curtsies.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 431fe4f09..f2ad92145 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -82,9 +82,9 @@ def request_refresh(when='now'):
def event_or_refresh(timeout=None):
if timeout is None:
- timeout = .2
+ timeout = 1
else:
- timeout = min(.2, timeout)
+ timeout = min(1, timeout)
starttime = time.time()
while True:
t = time.time()
From 4d058700b8a4033395d2211a9129d2427162fa1c Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 12:02:53 -0400
Subject: [PATCH 0096/1650] rename special_mode -> incremental_search_mode
---
bpython/curtsiesfrontend/repl.py | 29 ++++++++++++++---------------
1 file changed, 14 insertions(+), 15 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index ace8a66fc..cb15643cd 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -319,7 +319,7 @@ def smarter_request_reload(desc):
self.current_match = None # currently tab-selected autocompletion suggestion
self.list_win_visible = False # whether the infobox (suggestions, docstring) is visible
self.watching_files = False # auto reloading turned on
- self.special_mode = None # 'reverse_incremental_search' and 'incremental_search'
+ self.incremental_search_mode = None # 'reverse_incremental_search' and 'incremental_search'
self.incremental_search_target = ''
self.original_modules = sys.modules.keys()
@@ -493,7 +493,7 @@ def process_key_event(self, e):
self.incremental_search(reverse=True)
elif e in ("",):
self.incremental_search()
- elif e in ("", '') and self.special_mode:
+ elif e in ("", '') and self.incremental_search_mode:
self.add_to_incremental_search(self, backspace=True)
elif e in self.edit_keys.cut_buffer_edits:
self.readline_kill(e)
@@ -538,7 +538,7 @@ def process_key_event(self, e):
elif e in key_dispatch[self.config.edit_current_block_key]:
self.send_current_block_to_external_editor()
elif e in [""]: #ESC
- self.special_mode = None
+ self.incremental_search_mode = None
elif e in [""]:
self.add_normal_character(' ')
else:
@@ -558,7 +558,7 @@ def last_word(line):
reset_rl_history=False)
def incremental_search(self, reverse=False, include_current=False):
- if self.special_mode == None:
+ if self.incremental_search_mode == None:
self.rl_history.enter(self.current_line)
self.incremental_search_target = ''
else:
@@ -575,9 +575,9 @@ def incremental_search(self, reverse=False, include_current=False):
self._set_cursor_offset(len(self.current_line),
reset_rl_history=False, clear_special_mode=False)
if reverse:
- self.special_mode = 'reverse_incremental_search'
+ self.incremental_search_mode = 'reverse_incremental_search'
else:
- self.special_mode = 'incremental_search'
+ self.incremental_search_mode = 'incremental_search'
def readline_kill(self, e):
func = self.edit_keys[e]
@@ -733,7 +733,7 @@ def toggle_file_watch(self):
def add_normal_character(self, char):
if len(char) > 1 or is_nop(char):
return
- if self.special_mode:
+ if self.incremental_search_mode:
self.add_to_incremental_search(char)
else:
self.current_line = (self.current_line[:self.cursor_offset] +
@@ -755,9 +755,9 @@ def add_to_incremental_search(self, char=None, backspace=False):
self.incremental_search_target = self.incremental_search_target[:-1]
else:
self.incremental_search_target += char
- if self.special_mode == 'reverse_incremental_search':
+ if self.incremental_search_mode == 'reverse_incremental_search':
self.incremental_search(reverse=True, include_current=True)
- elif self.special_mode == 'incremental_search':
+ elif self.incremental_search_mode == 'incremental_search':
self.incremental_search(include_current=True)
else:
raise ValueError('add_to_incremental_search should only be called in a special mode')
@@ -849,7 +849,7 @@ def run_code_and_maybe_finish(self, for_code=None):
if err:
indent = 0
-
+
#TODO This should be printed ABOVE the error that just happened instead
# or maybe just thrown away and not shown
if self.current_stdouterr_line:
@@ -935,7 +935,7 @@ def current_line_formatted(self):
"""The colored current line (no prompt, not wrapped)"""
if self.config.syntax:
fs = bpythonparse(format(self.tokenize(self.current_line), self.formatter))
- if self.special_mode:
+ if self.incremental_search_mode:
if self.incremental_search_target in self.current_line:
fs = fmtfuncs.on_magenta(self.incremental_search_target).join(fs.split(self.incremental_search_target))
elif self.rl_history.saved_line and self.rl_history.saved_line in self.current_line:
@@ -969,10 +969,10 @@ def display_buffer_lines(self):
@property
def display_line_with_prompt(self):
"""colored line with prompt"""
- if self.special_mode == 'reverse_incremental_search':
+ if self.incremental_search_mode == 'reverse_incremental_search':
return func_for_letter(self.config.color_scheme['prompt'])(
'(reverse-i-search)`%s\': ' % (self.incremental_search_target,)) + self.current_line_formatted
- elif self.special_mode == 'incremental_search':
+ elif self.incremental_search_mode == 'incremental_search':
return func_for_letter(self.config.color_scheme['prompt'])(
'(i-search)`%s\': ' % (self.incremental_search_target,)) + self.current_line_formatted
return (func_for_letter(self.config.color_scheme['prompt'])(self.ps1)
@@ -1019,7 +1019,6 @@ def paint(self, about_to_exit=False, user_quit=False):
less state is cool!
"""
# The hairiest function in the curtsies - a cleanup would be great.
-
if about_to_exit:
self.clean_up_current_line_for_exit() # exception to not changing state!
@@ -1234,7 +1233,7 @@ def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=Fa
if reset_rl_history:
self.rl_history.reset()
if clear_special_mode:
- self.special_mode = None
+ self.incremental_search_mode = None
self._cursor_offset = offset
self.update_completion()
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,
From f8b56d16997ed2fd5407ef45fb55e0808bbedc2e Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 02:52:43 -0400
Subject: [PATCH 0097/1650] add failing test
---
bpython/test/test_curtsies_painting.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 098a9d31f..26c29fedf 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -450,3 +450,16 @@ def test_cursor_stays_at_bottom_of_screen(self):
u"'random'",
u">>> "]
self.assert_paint_ignoring_formatting(screen, (2, 4))
+
+ def test_unhighlight_paren_bug(self):
+ """infobox showing up during intermediate render was causing this to fail, #371"""
+ self.enter('(')
+ screen = [u">>> (",
+ u"... "]
+ self.assert_paint_ignoring_formatting(screen)
+
+ with output_to_repl(self.repl):
+ self.repl.process_event(')')
+ screen = fsarray([cyan(u">>> ")+yellow('('),
+ green(u"... ")+yellow(')')])
+ self.assert_paint(screen, (1, 3))
From 81c2197e1beecfd8ddb86586ae48c49ef4ee0949 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 11:58:29 -0400
Subject: [PATCH 0098/1650] fix #370
---
bpython/curtsiesfrontend/repl.py | 14 +++++++------
bpython/test/test_curtsies_painting.py | 28 +++++++++++++++++++-------
2 files changed, 29 insertions(+), 13 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index cb15643cd..f11c9a505 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -594,8 +594,6 @@ def on_enter(self, insert_into_history=True):
self.unhighlight_paren() # in unhighlight_paren
self.highlighted_paren = None
- self.rl_history.append(self.current_line)
- self.rl_history.last()
self.history.append(self.current_line)
self.push(self.current_line, insert_into_history=insert_into_history)
@@ -1038,6 +1036,10 @@ def paint(self, about_to_exit=False, user_quit=False):
arr = FSArray(0, width)
#TODO test case of current line filling up the whole screen (there aren't enough rows to show it)
+ current_line = paint.paint_current_line(min_height, width, self.current_cursor_line)
+ # needs to happen before we calculate contents of history because calculating
+ # self.current_cursor_line has the side effect of unhighlighting parens in buffer
+
def move_screen_up(current_line_start_row):
# move screen back up a screen minus a line
while current_line_start_row < 0:
@@ -1084,12 +1086,8 @@ def move_screen_up(current_line_start_row):
history = paint.paint_history(current_line_start_row, width, self.lines_for_display)
arr[:history.height,:history.width] = history
-
-
-
self.inconsistent_history = False
- current_line = paint.paint_current_line(min_height, width, self.current_cursor_line)
if user_quit: # quit() or exit() in interp
current_line_start_row = current_line_start_row - current_line.height
logger.debug("---current line row slice %r, %r", current_line_start_row, current_line_start_row + current_line.height)
@@ -1223,11 +1221,14 @@ def _set_current_line(self, line, update_completion=True, reset_rl_history=True,
self.rl_history.reset()
if clear_special_mode:
self.special_mode = None
+ self.unhighlight_paren()
current_line = property(_get_current_line, _set_current_line, None,
"The current line")
def _get_cursor_offset(self):
return self._cursor_offset
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=False, clear_special_mode=True):
+ if self._cursor_offset == offset:
+ return
if update_completion:
self.update_completion()
if reset_rl_history:
@@ -1236,6 +1237,7 @@ def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=Fa
self.incremental_search_mode = None
self._cursor_offset = offset
self.update_completion()
+ self.unhighlight_paren()
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,
"The current cursor offset from the front of the line")
def echo(self, msg, redraw=True):
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 26c29fedf..15018dff1 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -62,7 +62,7 @@ def test_run_line(self):
orig_stdout = sys.stdout
sys.stdout = self.repl.stdout
[self.repl.add_normal_character(c) for c in '1 + 1']
- self.repl.on_enter()
+ self.repl.on_enter(insert_into_history=False)
screen = fsarray([u'>>> 1 + 1', '2', 'Welcome to'])
self.assert_paint_ignoring_formatting(screen, (1, 1))
finally:
@@ -105,7 +105,8 @@ def enter(self, line=None):
if line is not None:
self.repl.current_line = line
with output_to_repl(self.repl):
- self.repl.on_enter()
+ self.repl.on_enter(insert_into_history=False)
+ self.assertEqual(self.repl.rl_history.entries, [''])
self.send_refreshes()
def undo(self):
@@ -432,7 +433,7 @@ def test_cursor_stays_at_bottom_of_screen(self):
self.repl.width = 50
self.repl.current_line = "__import__('random').__name__"
with output_to_repl(self.repl):
- self.repl.on_enter()
+ self.repl.on_enter(insert_into_history=False)
screen = [u">>> __import__('random').__name__",
u"'random'"]
self.assert_paint_ignoring_formatting(screen)
@@ -451,15 +452,28 @@ def test_cursor_stays_at_bottom_of_screen(self):
u">>> "]
self.assert_paint_ignoring_formatting(screen, (2, 4))
- def test_unhighlight_paren_bug(self):
- """infobox showing up during intermediate render was causing this to fail, #371"""
+ def test_unhighlight_paren_bugs(self):
+ """two previous bugs, paren did't highlight until next render
+ and paren didn't unhighlight until enter"""
+ self.assertEqual(self.repl.rl_history.entries, [''])
self.enter('(')
+ self.assertEqual(self.repl.rl_history.entries, [''])
screen = [u">>> (",
u"... "]
+ self.assertEqual(self.repl.rl_history.entries, [''])
self.assert_paint_ignoring_formatting(screen)
+ self.assertEqual(self.repl.rl_history.entries, [''])
with output_to_repl(self.repl):
+ self.assertEqual(self.repl.rl_history.entries, [''])
self.repl.process_event(')')
+ self.assertEqual(self.repl.rl_history.entries, [''])
+ screen = fsarray([cyan(u">>> ")+on_magenta(bold(red('('))),
+ green(u"... ")+on_magenta(bold(red(')')))])
+ self.assert_paint(screen, (1, 5))
+
+ with output_to_repl(self.repl):
+ self.repl.process_event(' ')
screen = fsarray([cyan(u">>> ")+yellow('('),
- green(u"... ")+yellow(')')])
- self.assert_paint(screen, (1, 3))
+ green(u"... ")+yellow(')')+bold(cyan(" "))])
+ self.assert_paint(screen, (1, 6))
From 329f3706c5810aff51490cc3b4f7b8f55df7fc71 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 18 Oct 2014 14:40:02 -0400
Subject: [PATCH 0099/1650] allow more key configuration, prevent doublebinds
---
bpython/config.py | 79 ++++++++++++++-------
bpython/curtsiesfrontend/manual_readline.py | 17 ++---
bpython/curtsiesfrontend/repl.py | 3 +-
bpython/sample-config | 3 +
bpython/test/test_config.py | 13 +++-
5 files changed, 78 insertions(+), 37 deletions(-)
diff --git a/bpython/config.py b/bpython/config.py
index f046d0ca5..a6f0e3569 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -35,7 +35,7 @@ def loadini(struct, configfile):
config_path = os.path.expanduser(configfile)
config = ConfigParser()
- fill_config_with_default_values(config, {
+ defaults = {
'general': {
'arg_spec': True,
'auto_display_list': True,
@@ -61,8 +61,15 @@ def loadini(struct, configfile):
'editor': os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
},
'keyboard': {
+ 'backspace': 'C-h',
+ 'left': 'C-b',
+ 'right': 'C-f',
+ 'beginning_of_line': 'C-a',
+ 'end_of_line': 'C-e',
+ 'transpose_chars': 'C-t',
'clear_line': 'C-u',
'clear_screen': 'C-l',
+ 'kill_line': 'C-k',
'clear_word': 'C-w',
'cut_to_buffer': 'C-k',
'delete': 'C-d',
@@ -90,7 +97,8 @@ def loadini(struct, configfile):
'curtsies': {
'list_above' : False,
'right_arrow_completion' : True,
- }})
+ }}
+ fill_config_with_default_values(config, defaults)
if not config.read(config_path):
# No config file. If the user has it in the old place then complain
if os.path.isfile(os.path.expanduser('~/.bpython.ini')):
@@ -99,6 +107,18 @@ def loadini(struct, configfile):
"%s\n" % default_config_path())
sys.exit(1)
+ def get_key_no_doublebind(attr, already_used={}):
+ """Clears any other configured keybindings using this key"""
+ key = config.get('keyboard', attr)
+ if key in already_used:
+ default = defaults['keyboard'][already_used[key]]
+ if default in already_used:
+ setattr(struct, '%s_key' % already_used[key], '')
+ else:
+ setattr(struct, '%s_key' % already_used[key], default)
+ already_used[key] = attr
+ return key
+
struct.config_path = config_path
struct.dedent_after = config.getint('general', 'dedent_after')
@@ -115,28 +135,39 @@ def loadini(struct, configfile):
struct.hist_length = config.getint('general', 'hist_length')
struct.hist_duplicates = config.getboolean('general', 'hist_duplicates')
struct.flush_output = config.getboolean('general', 'flush_output')
- struct.pastebin_key = config.get('keyboard', 'pastebin')
- struct.save_key = config.get('keyboard', 'save')
- struct.search_key = config.get('keyboard', 'search')
- struct.show_source_key = config.get('keyboard', 'show_source')
- struct.suspend_key = config.get('keyboard', 'suspend')
- struct.toggle_file_watch_key = config.get('keyboard', 'toggle_file_watch')
- struct.undo_key = config.get('keyboard', 'undo')
- struct.reimport_key = config.get('keyboard', 'reimport')
- struct.up_one_line_key = config.get('keyboard', 'up_one_line')
- struct.down_one_line_key = config.get('keyboard', 'down_one_line')
- struct.cut_to_buffer_key = config.get('keyboard', 'cut_to_buffer')
- struct.yank_from_buffer_key = config.get('keyboard', 'yank_from_buffer')
- struct.clear_word_key = config.get('keyboard', 'clear_word')
- struct.clear_line_key = config.get('keyboard', 'clear_line')
- struct.clear_screen_key = config.get('keyboard', 'clear_screen')
- struct.delete_key = config.get('keyboard', 'delete')
- struct.exit_key = config.get('keyboard', 'exit')
- struct.last_output_key = config.get('keyboard', 'last_output')
- struct.edit_config_key = config.get('keyboard', 'edit_config')
- struct.edit_current_block_key = config.get('keyboard', 'edit_current_block')
- struct.external_editor_key = config.get('keyboard', 'external_editor')
- struct.help_key = config.get('keyboard', 'help')
+
+ struct.pastebin_key = get_key_no_doublebind('pastebin')
+ struct.save_key = get_key_no_doublebind('save')
+ struct.search_key = get_key_no_doublebind('search')
+ struct.show_source_key = get_key_no_doublebind('show_source')
+ struct.suspend_key = get_key_no_doublebind('suspend')
+ struct.toggle_file_watch_key = get_key_no_doublebind('toggle_file_watch')
+ struct.undo_key = get_key_no_doublebind('undo')
+ struct.reimport_key = get_key_no_doublebind('reimport')
+ struct.up_one_line_key = get_key_no_doublebind('up_one_line')
+ struct.down_one_line_key = get_key_no_doublebind('down_one_line')
+ struct.cut_to_buffer_key = get_key_no_doublebind('cut_to_buffer')
+ struct.yank_from_buffer_key = get_key_no_doublebind('yank_from_buffer')
+ struct.clear_word_key = get_key_no_doublebind('clear_word')
+ struct.backspace_key = get_key_no_doublebind('backspace')
+ struct.clear_line_key = get_key_no_doublebind('clear_line')
+ struct.clear_screen_key = get_key_no_doublebind('clear_screen')
+ struct.delete_key = get_key_no_doublebind('delete')
+
+ struct.left_key = get_key_no_doublebind('left')
+ struct.right_key = get_key_no_doublebind('right')
+ struct.end_of_line_key = get_key_no_doublebind('end_of_line')
+ struct.beginning_of_line_key = get_key_no_doublebind('beginning_of_line')
+ struct.transpose_chars_key = get_key_no_doublebind('transpose_chars')
+ struct.clear_line_key = get_key_no_doublebind('clear_line')
+ struct.clear_screen_key = get_key_no_doublebind('clear_screen')
+ struct.kill_line_key = get_key_no_doublebind('kill_line')
+ struct.exit_key = get_key_no_doublebind('exit')
+ struct.last_output_key = get_key_no_doublebind('last_output')
+ struct.edit_config_key = get_key_no_doublebind('edit_config')
+ struct.edit_current_block_key = get_key_no_doublebind('edit_current_block')
+ struct.external_editor_key = get_key_no_doublebind('external_editor')
+ struct.help_key = get_key_no_doublebind('help')
struct.pastebin_confirm = config.getboolean('general', 'pastebin_confirm')
struct.pastebin_url = config.get('general', 'pastebin_url')
diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py
index 4be8bb961..3c608b14d 100644
--- a/bpython/curtsiesfrontend/manual_readline.py
+++ b/bpython/curtsiesfrontend/manual_readline.py
@@ -141,22 +141,22 @@ def kills_ahead(func):
func.kills = 'ahead'
return func
-@edit_keys.on('')
+@edit_keys.on(config='left_key')
@edit_keys.on('')
def left_arrow(cursor_offset, line):
return max(0, cursor_offset - 1), line
-@edit_keys.on('')
+@edit_keys.on(config='right_key')
@edit_keys.on('')
def right_arrow(cursor_offset, line):
return min(len(line), cursor_offset + 1), line
-@edit_keys.on('')
+@edit_keys.on(config='beginning_of_line_key')
@edit_keys.on('')
def beginning_of_line(cursor_offset, line):
return 0, line
-@edit_keys.on('')
+@edit_keys.on(config='end_of_line_key')
@edit_keys.on('')
def end_of_line(cursor_offset, line):
return len(line), line
@@ -188,9 +188,8 @@ def delete(cursor_offset, line):
return (cursor_offset,
line[:cursor_offset] + line[cursor_offset+1:])
-@edit_keys.on('')
@edit_keys.on('')
-@edit_keys.on(config='delete_key')
+@edit_keys.on(config='backspace_key')
def backspace(cursor_offset, line):
if cursor_offset == 0:
return cursor_offset, line
@@ -201,7 +200,6 @@ def backspace(cursor_offset, line):
return (cursor_offset - 1,
line[:cursor_offset - 1] + line[cursor_offset:])
-@edit_keys.on('')
@edit_keys.on(config='clear_line_key')
def delete_from_cursor_back(cursor_offset, line):
return 0, line[cursor_offset:]
@@ -215,7 +213,6 @@ def delete_rest_of_word(cursor_offset, line):
return (cursor_offset, line[:cursor_offset] + line[m.start()+cursor_offset+1:],
line[cursor_offset:m.start()+cursor_offset+1])
-@edit_keys.on('')
@edit_keys.on(config='clear_word_key')
@kills_behind
def delete_word_to_cursor(cursor_offset, line):
@@ -231,7 +228,7 @@ def yank_prev_prev_killed_text(cursor_offset, line, cut_buffer): #TODO not imple
def yank_prev_killed_text(cursor_offset, line, cut_buffer):
return cursor_offset+len(cut_buffer), line[:cursor_offset] + cut_buffer + line[cursor_offset:]
-@edit_keys.on('')
+@edit_keys.on(config='transpose_chars_key')
def transpose_character_before_cursor(cursor_offset, line):
return (min(len(line), cursor_offset + 1),
line[:cursor_offset-1] +
@@ -253,7 +250,7 @@ def delete_line(cursor_offset, line):
def uppercase_next_word(cursor_offset, line):
return cursor_offset, line #TODO Not implemented
-@edit_keys.on('')
+@edit_keys.on(config='kill_line_key')
@kills_ahead
def delete_from_cursor_forward(cursor_offset, line):
return cursor_offset, line[:cursor_offset], line[cursor_offset:]
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index f11c9a505..c682bb052 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -493,7 +493,8 @@ def process_key_event(self, e):
self.incremental_search(reverse=True)
elif e in ("",):
self.incremental_search()
- elif e in ("", '') and self.incremental_search_mode:
+ elif (e in ("",) + key_dispatch[self.config.backspace_key]
+ and self.incremental_search_mode):
self.add_to_incremental_search(self, backspace=True)
elif e in self.edit_keys.cut_buffer_edits:
self.readline_kill(e)
diff --git a/bpython/sample-config b/bpython/sample-config
index e75378649..88084e891 100644
--- a/bpython/sample-config
+++ b/bpython/sample-config
@@ -49,6 +49,8 @@ auto_display_list = True
[keyboard]
+# All key bindings are shown commented out with their default binding
+
# pastebin = F8
# last_output = F9
# reimport = F6
@@ -61,6 +63,7 @@ auto_display_list = True
# cut_to_buffer = C-k
# search = C-o
# yank_from_buffer = C-y
+# backspace = C-h
# clear_word = C-w
# clear_line = C-u
# clear_screen = C-l
diff --git a/bpython/test/test_config.py b/bpython/test/test_config.py
index d36ffe814..9bacc1f06 100644
--- a/bpython/test/test_config.py
+++ b/bpython/test/test_config.py
@@ -1,5 +1,6 @@
import os
import unittest
+import tempfile
from bpython import config
@@ -18,5 +19,13 @@ def test_load_theme(self):
config.load_theme(struct, TEST_THEME_PATH, struct.color_scheme, defaults)
self.assertEquals(struct.color_scheme, expected)
-if __name__ == '__main__':
- unittest.main()
+ def test_load_config(self):
+ struct = config.Struct()
+ with tempfile.NamedTemporaryFile() as f:
+ f.write(''.encode('utf8'))
+ f.write('[keyboard]\nhelp = C-h\n'.encode('utf8'))
+ f.flush()
+ config.loadini(struct, f.name)
+ self.assertEqual(struct.help_key, 'C-h')
+ self.assertEqual(struct.backspace_key, '')
+
From cb0a166a3102fc0a45890e0c401f13b0108c7e9f Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Mon, 20 Oct 2014 16:37:31 +0200
Subject: [PATCH 0100/1650] Document pastebin_helper a bit better
Signed-off-by: Sebastian Ramacher
---
bpython/sample-config | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/bpython/sample-config b/bpython/sample-config
index 88084e891..46c3460d9 100644
--- a/bpython/sample-config
+++ b/bpython/sample-config
@@ -12,7 +12,7 @@
# Display the autocomplete list as you type (default: True).
# When this is off, you can hit tab to see the suggestions.
-auto_display_list = True
+# auto_display_list = True
# Syntax highlighting as you type (default: True).
# syntax = True
@@ -44,7 +44,8 @@ auto_display_list = True
# (default: False)
# save_append_py = False
-# The name of a helper executable that should perform pastebin upload on bpython’s behalf.
+# The name of a helper executable that should perform pastebin upload on
+# bpython’s behalf. If unset, bpython uploads pastes to bpaste.net. (default: )
#pastebin_helper = gist.py
[keyboard]
@@ -70,8 +71,8 @@ auto_display_list = True
# show_source = F2
# exit = C-d
# external_editor = F7
-# edit_config = F3
-#
+# edit_config = F3
+
[curtsies]
# Allow the the completion and docstring box above the current line
From 05a2d40ddc9b2063c6053bed5df677bc90f7a092 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Mon, 20 Oct 2014 21:28:51 +0200
Subject: [PATCH 0101/1650] Raise an exception if safe_eval fails instead of
returning a sentinel.
---
bpython/autocomplete.py | 31 +++++++++++++++++--------------
bpython/test/test_autocomplete.py | 5 ++---
2 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index 73868c51f..914eef150 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -205,8 +205,9 @@ def matches(cls, cursor_offset, line, locals_, **kwargs):
return None
start, end, orig = r
_, _, dexpr = lineparts.current_dict(cursor_offset, line)
- obj = safe_eval(dexpr, locals_)
- if obj is SafeEvalFailed:
+ try:
+ obj = safe_eval(dexpr, locals_)
+ except EvaluationError:
return []
if obj and isinstance(obj, type({})) and obj.keys():
return ["{!r}]".format(k) for k in obj.keys() if repr(k).startswith(orig)]
@@ -288,23 +289,20 @@ def matches(cls, cursor_offset, line, **kwargs):
return [match for match in matches if not match.startswith('_')]
return matches
-class SafeEvalFailed(Exception):
- """If this object is returned, safe_eval failed"""
- # Because every normal Python value is a possible return value of safe_eval
+
+class EvaluationError(Exception):
+ """Raised if an exception occurred in safe_eval."""
+
def safe_eval(expr, namespace):
"""Not all that safe, just catches some errors"""
- if expr.isdigit():
- # Special case: float literal, using attrs here will result in
- # a SyntaxError
- return SafeEvalFailed
try:
- obj = eval(expr, namespace)
- return obj
+ return eval(expr, namespace)
except (NameError, AttributeError, SyntaxError):
# If debugging safe_eval, raise this!
# raise
- return SafeEvalFailed
+ raise EvaluationError
+
def attr_matches(text, namespace, autocomplete_mode):
"""Taken from rlcompleter.py and bent to my will.
@@ -318,8 +316,13 @@ def attr_matches(text, namespace, autocomplete_mode):
return []
expr, attr = m.group(1, 3)
- obj = safe_eval(expr, namespace)
- if obj is SafeEvalFailed:
+ if expr.isdigit():
+ # Special case: float literal, using attrs here will result in
+ # a SyntaxError
+ return []
+ try:
+ obj = safe_eval(expr, namespace)
+ except EvaluationError:
return []
with inspection.AttrCleaner(obj):
matches = attr_lookup(obj, expr, attr, autocomplete_mode)
diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py
index fc47a251a..3a4ee4cc7 100644
--- a/bpython/test/test_autocomplete.py
+++ b/bpython/test/test_autocomplete.py
@@ -14,10 +14,9 @@ def skip(f):
class TestSafeEval(unittest.TestCase):
def test_catches_syntax_error(self):
- try:
+ with self.assertRaises(autocomplete.EvaluationError):
autocomplete.safe_eval('1re',{})
- except:
- self.fail('safe_eval raises an error')
+
class TestFormatters(unittest.TestCase):
From acdbb1a9ca73b43b2a56b9372ded6859f5945721 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Mon, 20 Oct 2014 21:36:19 +0200
Subject: [PATCH 0102/1650] Make test work under Python 2.6.
---
bpython/test/test_autocomplete.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py
index 3a4ee4cc7..1f7d91b78 100644
--- a/bpython/test/test_autocomplete.py
+++ b/bpython/test/test_autocomplete.py
@@ -14,8 +14,8 @@ def skip(f):
class TestSafeEval(unittest.TestCase):
def test_catches_syntax_error(self):
- with self.assertRaises(autocomplete.EvaluationError):
- autocomplete.safe_eval('1re',{})
+ self.assertRaises(autocomplete.EvaluationError,
+ autocomplete.safe_eval, '1re', {})
class TestFormatters(unittest.TestCase):
From 3ce0ad3d9879f22559de8b77beeca68563bc0db3 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Thu, 23 Oct 2014 12:37:44 -0400
Subject: [PATCH 0103/1650] fix timed refresh behavior, but ugly
---
bpython/curtsies.py | 47 ++++++++++++++++++++++++++++++---------------
1 file changed, 32 insertions(+), 15 deletions(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index f2ad92145..40a4d27fb 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -82,22 +82,39 @@ def request_refresh(when='now'):
def event_or_refresh(timeout=None):
if timeout is None:
- timeout = 1
- else:
- timeout = min(1, timeout)
- starttime = time.time()
+ timeout = 2**25 # a year
while True:
- t = time.time()
- refresh_requests.sort(key=lambda r: 0 if r.when == 'now' else r.when)
- if refresh_requests and (refresh_requests[0].when == 'now' or refresh_requests[-1].when < t):
- yield refresh_requests.pop(0)
- elif reload_requests:
- e = reload_requests.pop()
- yield e
- else:
- e = input_generator.send(timeout)
- if starttime + timeout < time.time() or e is not None:
+ starttime = time.time()
+ while True:
+ t = time.time()
+ refresh_requests.sort(key=lambda r: 0 if r.when == 'now' else r.when)
+ if refresh_requests and (refresh_requests[0].when == 'now' or refresh_requests[-1].when < t):
+ yield refresh_requests.pop(0)
+ elif reload_requests:
+ e = reload_requests.pop()
yield e
+ else:
+ if refresh_requests:
+ next_refresh = refresh_requests.pop(0)
+ time_until_next_scheduled_event = max(0, next_refresh.when - t)
+ else:
+ next_refresh = None
+ time_until_next_scheduled_event = 2**25
+
+ time_to_wait = min(time_until_next_scheduled_event, max(0, starttime + timeout - t))
+
+ e = input_generator.send(time_to_wait)
+
+ if next_refresh is not None:
+ if e is None and time.time() > t + time_until_next_scheduled_event:
+ yield next_refresh
+ continue
+ else:
+ refresh_requests.insert(0, next_refresh)
+
+ if starttime + timeout < time.time() or e is not None:
+ yield e
+ break
def on_suspend():
window.__exit__(None, None, None)
@@ -144,7 +161,7 @@ def process_event(e):
for _, e in izip(find_iterator, event_or_refresh(0)):
if e is not None:
process_event(e)
- for e in event_or_refresh():
+ for e in event_or_refresh(None):
process_event(e)
if __name__ == '__main__':
From 700843917e8dc09abd2416fa8d66bbec9bd530c0 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sun, 26 Oct 2014 00:37:45 -0400
Subject: [PATCH 0104/1650] Refactor reactor into curtsies Input object
---
bpython/curtsies.py | 58 ++++++-------------------
bpython/curtsiesfrontend/coderunner.py | 13 +++---
bpython/curtsiesfrontend/events.py | 36 +++++++++++++++
bpython/curtsiesfrontend/filewatch.py | 2 +-
bpython/curtsiesfrontend/interaction.py | 12 +++--
bpython/curtsiesfrontend/repl.py | 44 ++++++++++++-------
bpython/test/test_curtsies_painting.py | 6 +--
7 files changed, 97 insertions(+), 74 deletions(-)
create mode 100644 bpython/curtsiesfrontend/events.py
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 40a4d27fb..4ba670b97 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -17,6 +17,10 @@
from bpython import args as bpargs
from bpython.translations import _
from bpython.importcompletion import find_iterator
+from bpython.curtsiesfrontend import events as bpythonevents
+
+logger = logging.getLogger(__name__)
+
repl = None # global for `from bpython.curtsies import repl`
#WARNING Will be a problem if more than one repl is ever instantiated this way
@@ -73,48 +77,9 @@ def mainloop(config, locals_, banner, interp=None, paste=None, interactive=True)
hide_cursor=False,
extra_bytes_callback=input_generator.unget_bytes) as window:
- reload_requests = []
- def request_reload(desc):
- reload_requests.append(curtsies.events.ReloadEvent([desc]))
- refresh_requests = []
- def request_refresh(when='now'):
- refresh_requests.append(curtsies.events.RefreshRequestEvent(when=when))
-
- def event_or_refresh(timeout=None):
- if timeout is None:
- timeout = 2**25 # a year
- while True:
- starttime = time.time()
- while True:
- t = time.time()
- refresh_requests.sort(key=lambda r: 0 if r.when == 'now' else r.when)
- if refresh_requests and (refresh_requests[0].when == 'now' or refresh_requests[-1].when < t):
- yield refresh_requests.pop(0)
- elif reload_requests:
- e = reload_requests.pop()
- yield e
- else:
- if refresh_requests:
- next_refresh = refresh_requests.pop(0)
- time_until_next_scheduled_event = max(0, next_refresh.when - t)
- else:
- next_refresh = None
- time_until_next_scheduled_event = 2**25
-
- time_to_wait = min(time_until_next_scheduled_event, max(0, starttime + timeout - t))
-
- e = input_generator.send(time_to_wait)
-
- if next_refresh is not None:
- if e is None and time.time() > t + time_until_next_scheduled_event:
- yield next_refresh
- continue
- else:
- refresh_requests.insert(0, next_refresh)
-
- if starttime + timeout < time.time() or e is not None:
- yield e
- break
+ request_refresh = input_generator.event_trigger(bpythonevents.RefreshRequestEvent)
+ schedule_refresh = input_generator.scheduled_event_trigger(bpythonevents.ScheduledRefreshRequestEvent)
+ request_reload = input_generator.threadsafe_event_trigger(bpythonevents.ReloadEvent)
def on_suspend():
window.__exit__(None, None, None)
@@ -128,6 +93,7 @@ def after_suspend():
with Repl(config=config,
locals_=locals_,
request_refresh=request_refresh,
+ schedule_refresh=schedule_refresh,
request_reload=request_reload,
get_term_hw=window.get_term_hw,
get_cursor_vertical_diff=window.get_cursor_vertical_diff,
@@ -157,11 +123,13 @@ def process_event(e):
if paste:
process_event(paste)
- process_event(None) #priming the pump (do a display before waiting for first event)
- for _, e in izip(find_iterator, event_or_refresh(0)):
+ process_event(None) # do a display before waiting for first event
+ for _ in find_iterator:
+ e = input_generator.send(0)
if e is not None:
process_event(e)
- for e in event_or_refresh(None):
+
+ for e in input_generator:
process_event(e)
if __name__ == '__main__':
diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py
index d2706cecc..e10897ee2 100644
--- a/bpython/curtsiesfrontend/coderunner.py
+++ b/bpython/curtsiesfrontend/coderunner.py
@@ -68,19 +68,19 @@ class CodeRunner(object):
just passes whatever is passed in to run_code(for_code) to the
code greenlet
"""
- def __init__(self, interp=None, stuff_a_refresh_request=lambda:None):
+ def __init__(self, interp=None, request_refresh=lambda:None):
"""
interp is an interpreter object to use. By default a new one is
created.
- stuff_a_refresh_request is a function that will be called each time
+ request_refresh is a function that will be called each time
the running code asks for a refresh - to, for example, update the screen.
"""
self.interp = interp or code.InteractiveInterpreter()
self.source = None
self.main_greenlet = greenlet.getcurrent()
self.code_greenlet = None
- self.stuff_a_refresh_request = stuff_a_refresh_request
+ self.request_refresh = request_refresh
self.code_is_waiting = False # waiting for response from main thread
self.sigint_happened_in_main_greenlet = False # sigint happened while in main thread
self.orig_sigint_handler = None
@@ -125,12 +125,13 @@ def run_code(self, for_code=None):
else:
request = self.code_greenlet.switch(for_code)
+ logger.debug('request received from code was %r', request)
if not issubclass(request, RequestFromCodeGreenlet):
raise ValueError("Not a valid value from code greenlet: %r" % request)
if request in [Wait, Refresh]:
self.code_is_waiting = True
if request == Refresh:
- self.stuff_a_refresh_request()
+ self.request_refresh()
return False
elif request in [Done, Unfinished]:
self._unload_code()
@@ -188,7 +189,7 @@ def isatty(self):
def test_simple():
orig_stdout = sys.stdout
orig_stderr = sys.stderr
- c = CodeRunner(stuff_a_refresh_request=lambda: orig_stdout.flush() or orig_stderr.flush())
+ c = CodeRunner(request_refresh=lambda: orig_stdout.flush() or orig_stderr.flush())
stdout = FakeOutput(c, orig_stdout.write)
sys.stdout = stdout
c.load_code('1 + 1')
@@ -199,7 +200,7 @@ def test_simple():
def test_exception():
orig_stdout = sys.stdout
orig_stderr = sys.stderr
- c = CodeRunner(stuff_a_refresh_request=lambda: orig_stdout.flush() or orig_stderr.flush())
+ c = CodeRunner(request_refresh=lambda: orig_stdout.flush() or orig_stderr.flush())
def ctrlc():
raise KeyboardInterrupt()
stdout = FakeOutput(c, lambda x: ctrlc())
diff --git a/bpython/curtsiesfrontend/events.py b/bpython/curtsiesfrontend/events.py
new file mode 100644
index 000000000..1b45c2956
--- /dev/null
+++ b/bpython/curtsiesfrontend/events.py
@@ -0,0 +1,36 @@
+"""Non-keybaord events used in bpython curtsies REPL"""
+import time
+
+import curtsies.events
+
+
+class ReloadEvent(curtsies.events.Event):
+ """Request to rerun REPL session ASAP because imported modules changed"""
+ def __init__(self, files_modified=('?',)):
+ self.files_modified = files_modified
+
+ def __repr__(self):
+ return "" % (' & '.join(self.files_modified))
+
+
+class RefreshRequestEvent(curtsies.events.Event):
+ """Request to refresh REPL display ASAP"""
+ def __init__(self, who='?'):
+ self.who = who
+
+ def __repr__(self):
+ return "" % (self.who,)
+
+
+class ScheduledRefreshRequestEvent(curtsies.events.ScheduledEvent):
+ """Request to refresh the REPL display at some point in the future
+
+ Used to schedule the dissapearance of status bar message that only show
+ for a few seconds"""
+ def __init__(self, when, who='?'):
+ self.who = who
+ self.when = when # time.time() + how long
+
+ def __repr__(self):
+ return ("" %
+ (self.who, self.when - time.time()))
diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py
index 73930730e..57955cbc5 100644
--- a/bpython/curtsiesfrontend/filewatch.py
+++ b/bpython/curtsiesfrontend/filewatch.py
@@ -63,7 +63,7 @@ def on_any_event(self, event):
dirpath = os.path.dirname(event.src_path)
paths = [path + '.py' for path in self.dirs[dirpath]]
if event.src_path in paths:
- self.on_change(event.src_path)
+ self.on_change(files_modified=[event.src_path])
if __name__ == '__main__':
m = ModuleChangedEventHandler([])
diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py
index c58eae11b..1036471c1 100644
--- a/bpython/curtsiesfrontend/interaction.py
+++ b/bpython/curtsiesfrontend/interaction.py
@@ -19,7 +19,10 @@ class StatusBar(BpythonInteraction):
functionality in a evented or callback style, but trying to integrate
bpython.Repl code.
"""
- def __init__(self, permanent_text="", refresh_request=lambda: None):
+ def __init__(self,
+ permanent_text="",
+ request_refresh=lambda: None,
+ schedule_refresh=lambda when: None):
self._current_line = ''
self.cursor_offset_in_line = 0
self.in_prompt = False
@@ -34,7 +37,8 @@ def __init__(self, permanent_text="", refresh_request=lambda: None):
self.permanent_stack.append(permanent_text)
self.main_greenlet = greenlet.getcurrent()
self.request_greenlet = None
- self.refresh_request = refresh_request
+ self.request_refresh = request_refresh
+ self.schedule_refresh = schedule_refresh
def push_permanent_message(self, msg):
self._message = ''
@@ -54,7 +58,7 @@ def message(self, msg):
"""Sets a temporary message"""
self.message_start_time = time.time()
self._message = msg
- self.refresh_request(time.time() + self.message_time)
+ self.schedule_refresh(time.time() + self.message_time)
def _check_for_expired_message(self):
if self._message and time.time() > self.message_start_time + self.message_time:
@@ -129,7 +133,7 @@ def notify(self, msg, n=3):
self.message_time = n
self.message(msg)
self.waiting_for_refresh = True
- self.refresh_request()
+ self.request_refresh()
self.main_greenlet.switch(msg)
# below Really ought to be called from greenlets other than main because they block
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index c682bb052..ae1604e53 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -43,6 +43,7 @@
from bpython.curtsiesfrontend.filewatch import ModuleChangedEventHandler
from bpython.curtsiesfrontend.interaction import StatusBar
from bpython.curtsiesfrontend.manual_readline import edit_keys
+from bpython.curtsiesfrontend import events as bpythonevents
#TODO other autocomplete modes (also fix in other bpython implementations)
@@ -213,8 +214,10 @@ class Repl(BpythonRepl):
def __init__(self,
locals_=None,
config=None,
- request_refresh=lambda when='now': None,
- request_reload=lambda desc: None, get_term_hw=lambda:(50, 10),
+ request_refresh=lambda: None,
+ schedule_refresh=lambda when=0: None,
+ request_reload=lambda desc: None,
+ get_term_hw=lambda:(50, 10),
get_cursor_vertical_diff=lambda: 0,
banner=None,
interp=None,
@@ -259,22 +262,30 @@ def __init__(self,
self.reevaluating = False
self.fake_refresh_requested = False
- def smarter_request_refresh(when='now'):
+ def smarter_request_refresh():
if self.reevaluating or self.paste_mode:
self.fake_refresh_requested = True
else:
- request_refresh(when=when)
+ request_refresh()
self.request_refresh = smarter_request_refresh
- def smarter_request_reload(desc):
+ def smarter_schedule_refresh(when='now'):
+ if self.reevaluating or self.paste_mode:
+ self.fake_refresh_requested = True
+ else:
+ schedule_refresh(when=when)
+ self.schedule_refresh = smarter_schedule_refresh
+ def smarter_request_reload(files_modified=()):
if self.watching_files:
- request_reload(desc)
+ request_reload(files_modified=files_modified)
else:
pass
self.request_reload = smarter_request_reload
self.get_term_hw = get_term_hw
self.get_cursor_vertical_diff = get_cursor_vertical_diff
- self.status_bar = StatusBar('', refresh_request=self.request_refresh)
+ self.status_bar = StatusBar('',
+ request_refresh=self.request_refresh,
+ schedule_refresh=self.schedule_refresh)
self.edit_keys = edit_keys.mapping_with_config(config, key_dispatch)
logger.debug("starting parent init")
super(Repl, self).__init__(interp, config)
@@ -423,18 +434,20 @@ def process_event(self, e):
logger.debug("processing event %r", e)
if isinstance(e, events.Event):
- return self.proccess_control_event(e)
+ return self.process_control_event(e)
else:
self.last_events.append(e)
self.last_events.pop(0)
return self.process_key_event(e)
- def proccess_control_event(self, e):
+ def process_control_event(self, e):
- if isinstance(e, events.RefreshRequestEvent):
- if e.when != 'now':
- pass # This is a scheduled refresh - it's really just a refresh (so nop)
- elif self.status_bar.has_focus:
+ if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent):
+ pass # This is a scheduled refresh - it's really just a refresh (so nop)
+
+ elif isinstance(e, bpythonevents.RefreshRequestEvent):
+ logger.info('received ASAP refresh request event')
+ if self.status_bar.has_focus:
self.status_bar.process_event(e)
else:
assert self.coderunner.code_is_waiting
@@ -463,7 +476,7 @@ def proccess_control_event(self, e):
self.keyboard_interrupt()
return
- elif isinstance(e, events.ReloadEvent):
+ elif isinstance(e, bpythonevents.ReloadEvent):
if self.watching_files:
self.clear_modules_and_reevaluate()
self.status_bar.message('Reloaded at ' + time.strftime('%H:%M:%S') + ' because ' + ' & '.join(e.files_modified) + ' modified')
@@ -1296,7 +1309,7 @@ def reevaluate(self, insert_into_history=False):
self.on_enter(insert_into_history=insert_into_history)
while self.fake_refresh_requested:
self.fake_refresh_requested = False
- self.process_event(events.RefreshRequestEvent())
+ self.process_event(bpythonevents.RefreshRequestEvent())
sys.stdin = self.stdin
self.reevaluating = False
@@ -1405,6 +1418,7 @@ def compress_paste_event(paste_event):
else:
return None
+#TODO this needs some work to function again and be useful for embedding
def simple_repl():
refreshes = []
def request_refresh():
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 15018dff1..815b5ebf1 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -11,7 +11,7 @@ def skip(f):
from curtsies.formatstringarray import FormatStringTest, fsarray
from curtsies.fmtfuncs import *
-from curtsies.events import RefreshRequestEvent
+from bpython.curtsiesfrontend.events import RefreshRequestEvent
from bpython import config
from bpython.curtsiesfrontend.repl import Repl
@@ -89,8 +89,8 @@ def output_to_repl(repl):
sys.stdout, sys.stderr = old_out, old_err
class TestCurtsiesRewindRedraw(TestCurtsiesPainting):
- def refresh(self, when='now'):
- self.refresh_requests.append(RefreshRequestEvent(when=when))
+ def refresh(self):
+ self.refresh_requests.append(RefreshRequestEvent())
def send_refreshes(self):
while self.refresh_requests:
From a91e44abf4219b2c16129f91397afcd941f4f55f Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sun, 26 Oct 2014 15:24:40 -0400
Subject: [PATCH 0105/1650] bump version to ensure input reactor in curtsies
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 685894e90..9e67146fe 100755
--- a/setup.py
+++ b/setup.py
@@ -153,7 +153,7 @@ def initialize_options(self):
if sys.version_info[:2] >= (2, 6):
# curtsies only supports 2.6 and onwards
- extras_require['curtsies'] = ['curtsies >=0.1.11, <0.2.0', 'greenlet']
+ extras_require['curtsies'] = ['curtsies >=0.1.15, <0.2.0', 'greenlet']
extras_require['watch'] = ['watchdog']
packages.append("bpython.curtsiesfrontend")
entry_points['console_scripts'].append(
From 42f3e540bdbd33722d928ca7b760bd68399d79c0 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sun, 26 Oct 2014 16:08:41 -0400
Subject: [PATCH 0106/1650] improve formatting
---
bpython/curtsies.py | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index 4ba670b97..ec9a30a0e 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -3,9 +3,7 @@
import code
import logging
import sys
-import time
from optparse import Option
-from itertools import izip
import curtsies
import curtsies.window
@@ -22,16 +20,17 @@
logger = logging.getLogger(__name__)
-repl = None # global for `from bpython.curtsies import repl`
-#WARNING Will be a problem if more than one repl is ever instantiated this way
+repl = None # global for `from bpython.curtsies import repl`
+# WARNING Will be a problem if more than one repl is ever instantiated this way
+
def main(args=None, locals_=None, banner=None):
config, options, exec_args = bpargs.parse(args, (
'curtsies options', None, [
Option('--log', '-L', action='store_true',
- help=_("log debug messages to bpython.log")),
+ help=_("log debug messages to bpython.log")),
Option('--type', '-t', action='store_true',
- help=_("enter lines of file as though interactively typed")),
+ help=_("enter lines of file as though interactively typed")),
]))
if options.log:
handler = logging.FileHandler(filename='bpython.log')
@@ -63,7 +62,7 @@ def main(args=None, locals_=None, banner=None):
if not options.interactive:
raise SystemExit(exit_value)
else:
- sys.path.insert(0, '') # expected for interactive sessions (vanilla python does it)
+ sys.path.insert(0, '') # expected for interactive sessions (vanilla python does it)
print(bpargs.version_banner())
mainloop(config, locals_, banner, interp, paste, interactive=(not exec_args))
@@ -123,7 +122,7 @@ def process_event(e):
if paste:
process_event(paste)
- process_event(None) # do a display before waiting for first event
+ process_event(None) # do a display before waiting for first event
for _ in find_iterator:
e = input_generator.send(0)
if e is not None:
From 6d31d46e0ab2cc1ab5a1d8ad950b7ed18acc8f8a Mon Sep 17 00:00:00 2001
From: Simon de Vlieger
Date: Tue, 28 Oct 2014 13:20:00 +0100
Subject: [PATCH 0107/1650] Add a fallthrough sourcecodenotfound message
---
bpython/repl.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/bpython/repl.py b/bpython/repl.py
index 13c06fb5c..6b7d8f6d8 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -594,6 +594,7 @@ def get_source_of_current_name(self):
current name in the current input line. Throw `SourceNotFound` if the
source cannot be found."""
obj = self.current_func
+ msg = "Unexpected error retrieving source code for %s" % (obj,)
try:
if obj is None:
line = self.current_line
From 7c274fe0ee9c69476707f7b723a1a3a335738efd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Tue, 28 Oct 2014 15:02:39 +0100
Subject: [PATCH 0108/1650] Fix indentation error.
Was introduced in e18e43787bfe26f58c503cc7738c7336409f1150.
---
bpython/repl.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 6b7d8f6d8..b767189d7 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -594,7 +594,6 @@ def get_source_of_current_name(self):
current name in the current input line. Throw `SourceNotFound` if the
source cannot be found."""
obj = self.current_func
- msg = "Unexpected error retrieving source code for %s" % (obj,)
try:
if obj is None:
line = self.current_line
@@ -602,7 +601,7 @@ def get_source_of_current_name(self):
raise SourceNotFound("Nothing to get source of")
if inspection.is_eval_safe_name(line):
obj = self.get_object(line)
- return inspect.getsource(obj)
+ return inspect.getsource(obj)
except (AttributeError, NameError), e:
msg = "Cannot get source: " + str(e)
except IOError, e:
From 934c511be5e251b762a69e724118a2ca8cfdeafb Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 22 Nov 2014 15:52:46 -0500
Subject: [PATCH 0109/1650] some tests for getting source
---
bpython/test/test_repl.py | 49 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 48 insertions(+), 1 deletion(-)
diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py
index 86692dc91..7eaa5315d 100644
--- a/bpython/test/test_repl.py
+++ b/bpython/test/test_repl.py
@@ -1,8 +1,12 @@
+import collections
+from itertools import islice
import os
+import socket
import sys
import unittest
-from itertools import islice
+
from mock import Mock, MagicMock
+
try:
from unittest import skip
except ImportError:
@@ -13,6 +17,7 @@ def skip(f):
from bpython import config, repl, cli, autocomplete
+
def setup_config(conf):
config_struct = config.Struct()
config.loadini(config_struct, os.devnull)
@@ -253,6 +258,48 @@ def test_nonexistent_name(self):
self.assertFalse(self.repl.get_args())
+class TestGetSource(unittest.TestCase):
+ def setUp(self):
+ self.repl = FakeRepl()
+
+ def set_input_line(self, line):
+ """Set current input line of the test REPL."""
+ self.repl.current_line = line
+ self.repl.cursor_offset = len(line)
+
+ def assert_get_source_error_for_current_function(self, func, msg):
+ self.repl.current_func = func
+ self.assertRaises(repl.SourceNotFound, self.repl.get_source_of_current_name)
+ try:
+ self.repl.get_source_of_current_name()
+ except repl.SourceNotFound as e:
+ self.assertEqual(e.args[0], msg)
+
+ def test_current_function(self):
+ self.set_input_line('INPUTLINE')
+ self.repl.current_func = collections.MutableSet.add
+ self.assertTrue("Add an element." in self.repl.get_source_of_current_name())
+
+ self.assert_get_source_error_for_current_function(
+ collections.defaultdict.copy, "No source code found for INPUTLINE")
+
+ self.assert_get_source_error_for_current_function(
+ collections.defaultdict, "could not find class definition")
+
+ self.assert_get_source_error_for_current_function(
+ [], "No source code found for INPUTLINE")
+
+ self.assert_get_source_error_for_current_function(
+ list.pop, "No source code found for INPUTLINE")
+
+ def test_current_line(self):
+ self.repl.interp.locals['a'] = socket.socket
+ self.set_input_line('a')
+ self.assertTrue('dup(self)' in self.repl.get_source_of_current_name())
+
+#TODO add tests for various failures without using current function
+
+
class TestRepl(unittest.TestCase):
def setInputLine(self, line):
From d7b95d988a00c11d3e4cc66239e929ed0e071a2a Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 22 Nov 2014 15:57:07 -0500
Subject: [PATCH 0110/1650] deal with more things being logged by curtsies now
---
bpython/curtsies.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index ec9a30a0e..d0282b92f 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -41,7 +41,8 @@ def main(args=None, locals_=None, banner=None):
logging.getLogger('bpython').addHandler(handler)
logging.getLogger('bpython').propagate = False
else:
- logging.getLogger('bpython').setLevel(logging.WARNING)
+ logging.getLogger('bpython').setLevel(logging.ERROR)
+ logging.getLogger('curtsies').setLevel(logging.ERROR)
interp = None
paste = None
From c131d617b09a2436e7e3685c565089061fd17f49 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sat, 22 Nov 2014 19:39:24 -0500
Subject: [PATCH 0111/1650] deleting of lines super-paste feature
---
bpython/curtsiesfrontend/repl.py | 98 +++++++++++++++++++++-----
bpython/test/test_curtsies_painting.py | 1 +
bpython/test/test_curtsies_repl.py | 14 ++--
bpython/test/test_interpreter.py | 13 ++++
4 files changed, 100 insertions(+), 26 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index ae1604e53..3735d7080 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -461,8 +461,11 @@ def process_control_event(self, e):
ctrl_char = compress_paste_event(e)
if ctrl_char is not None:
return self.process_event(ctrl_char)
+ simple_events = just_simple_events(e.events)
+ source = bad_empty_lines_removed(''.join(simple_events))
+
with self.in_paste_mode():
- for ee in e.events:
+ for ee in source:
if self.stdin.has_focus:
self.stdin.process_event(ee)
else:
@@ -712,7 +715,9 @@ def send_session_to_external_editor(self, filename=None):
for line in self.getstdout().split('\n'))
text = self.send_to_external_editor(for_editor)
lines = text.split('\n')
- self.history = [line for line in lines if line[:4] != '### ']
+ from_editor = [line for line in lines if line[:4] != '### ']
+ source = bad_empty_lines_removed('\n'.join(from_editor))
+ self.history = source.split('\n')
self.reevaluate(insert_into_history=True)
self.current_line = lines[-1][4:]
self.cursor_offset = len(self.current_line)
@@ -822,7 +827,7 @@ def push(self, line, insert_into_history=True):
code_to_run = '\n'.join(self.buffer)
logger.debug('running %r in interpreter', self.buffer)
- c, code_will_parse = self.buffer_finished_will_parse()
+ c, code_will_parse = code_finished_will_parse('\n'.join(self.buffer))
self.saved_predicted_parse_error = not code_will_parse
if c:
logger.debug('finished - buffer cleared')
@@ -834,21 +839,6 @@ def push(self, line, insert_into_history=True):
self.coderunner.load_code(code_to_run)
self.run_code_and_maybe_finish()
- def buffer_finished_will_parse(self):
- """Returns a tuple of whether the buffer could be complete and whether it will parse
-
- True, True means code block is finished and no predicted parse error
- True, False means code block is finished because a parse error is predicted
- False, True means code block is unfinished
- False, False isn't possible - an predicted error makes code block done"""
- try:
- finished = bool(code.compile_command('\n'.join(self.buffer)))
- code_will_parse = True
- except (ValueError, SyntaxError, OverflowError):
- finished = True
- code_will_parse = False
- return finished, code_will_parse
-
def run_code_and_maybe_finish(self, for_code=None):
r = self.coderunner.run_code(for_code=for_code)
if r:
@@ -861,7 +851,6 @@ def run_code_and_maybe_finish(self, for_code=None):
if err:
indent = 0
-
#TODO This should be printed ABOVE the error that just happened instead
# or maybe just thrown away and not shown
if self.current_stdouterr_line:
@@ -1418,6 +1407,77 @@ def compress_paste_event(paste_event):
else:
return None
+def just_simple_events(event_list):
+ simple_events = []
+ for e in event_list:
+ if e in (u"", u"", u"", u"\n", u"\r"): # '\n' necessary for pastes
+ simple_events.append(u'\n')
+ elif isinstance(e, events.Event):
+ pass # ignore events
+ elif e == '':
+ simple_events.append(' ')
+ else:
+ simple_events.append(e)
+ return simple_events
+
+def code_finished_will_parse(s):
+ """Returns a tuple of whether the buffer could be complete and whether it will parse
+
+ True, True means code block is finished and no predicted parse error
+ True, False means code block is finished because a parse error is predicted
+ False, True means code block is unfinished
+ False, False isn't possible - an predicted error makes code block done"""
+ try:
+ finished = bool(code.compile_command(s))
+ code_will_parse = True
+ except (ValueError, SyntaxError, OverflowError):
+ finished = True
+ code_will_parse = False
+ return finished, code_will_parse
+
+def bad_empty_lines_removed(s):
+ """Removes empty lines that would cause unfinished input to be evaluated"""
+ # If there's a syntax error followed by an empty line, remove the empty line
+ lines = s.split('\n')
+ #TODO this should be our interpreter object making this decision so it
+ # can be compiler directive (__future__ statement) -aware
+ #TODO specifically catch IndentationErrors instead of any syntax errors
+
+ current_block = []
+ complete_blocks = []
+ for i, line in enumerate(s.split('\n')):
+ current_block.append(line)
+ could_be_finished, valid = code_finished_will_parse('\n'.join(current_block))
+ if could_be_finished and valid:
+ complete_blocks.append(current_block)
+ current_block = []
+ continue
+ elif could_be_finished and not valid:
+ if complete_blocks:
+ complete_blocks[-1].extend(current_block)
+ current_block = complete_blocks.pop()
+ if len(current_block) < 2:
+ return s #TODO return partial result instead of giving up
+ last_line = current_block.pop(len(current_block) - 2)
+ assert not last_line, last_line
+ new_finished, new_valid = code_finished_will_parse('\n'.join(current_block))
+ if new_valid and new_finished:
+ complete_blocks.append(current_block)
+ current_block = []
+ elif new_valid:
+ continue
+ else:
+ return s #TODO return partial result instead of giving up
+
+ else:
+ return s #TODO return partial result instead of giving up
+ else:
+ continue
+ return '\n'.join(['\n'.join(block)
+ for block in complete_blocks + [current_block]
+ if block])
+
+
#TODO this needs some work to function again and be useful for embedding
def simple_repl():
refreshes = []
diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py
index 815b5ebf1..9a3823db4 100644
--- a/bpython/test/test_curtsies_painting.py
+++ b/bpython/test/test_curtsies_painting.py
@@ -1,4 +1,5 @@
# coding: utf8
+from __future__ import unicode_literals
import sys
import os
from contextlib import contextmanager
diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py
index 00652fa78..344cfcb20 100644
--- a/bpython/test/test_curtsies_repl.py
+++ b/bpython/test/test_curtsies_repl.py
@@ -33,19 +33,19 @@ class TestCurtsiesRepl(unittest.TestCase):
def setUp(self):
self.repl = create_repl()
- def test_buffer_finished_will_parse(self):
+ def test_code_finished_will_parse(self):
self.repl.buffer = ['1 + 1']
- self.assertTrue(self.repl.buffer_finished_will_parse(), (True, True))
+ self.assertTrue(curtsiesrepl.code_finished_will_parse('\n'.join(self.repl.buffer)), (True, True))
self.repl.buffer = ['def foo(x):']
- self.assertTrue(self.repl.buffer_finished_will_parse(), (False, True))
+ self.assertTrue(curtsiesrepl.code_finished_will_parse('\n'.join(self.repl.buffer)), (False, True))
self.repl.buffer = ['def foo(x)']
- self.assertTrue(self.repl.buffer_finished_will_parse(), (True, False))
+ self.assertTrue(curtsiesrepl.code_finished_will_parse('\n'.join(self.repl.buffer)), (True, False))
self.repl.buffer = ['def foo(x):', 'return 1']
- self.assertTrue(self.repl.buffer_finished_will_parse(), (True, False))
+ self.assertTrue(curtsiesrepl.code_finished_will_parse('\n'.join(self.repl.buffer)), (True, False))
self.repl.buffer = ['def foo(x):', ' return 1']
- self.assertTrue(self.repl.buffer_finished_will_parse(), (True, True))
+ self.assertTrue(curtsiesrepl.code_finished_will_parse('\n'.join(self.repl.buffer)), (True, True))
self.repl.buffer = ['def foo(x):', ' return 1', '']
- self.assertTrue(self.repl.buffer_finished_will_parse(), (True, True))
+ self.assertTrue(curtsiesrepl.code_finished_will_parse('\n'.join(self.repl.buffer)), (True, True))
def test_external_communication(self):
self.assertEqual(type(self.repl.help_text()), type(b''))
diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py
index bb2cc29cb..56a8806e5 100644
--- a/bpython/test/test_interpreter.py
+++ b/bpython/test/test_interpreter.py
@@ -1,6 +1,7 @@
import unittest
from bpython.curtsiesfrontend import interpreter
+from bpython.curtsiesfrontend.repl import bad_empty_lines_removed
from curtsies.fmtfuncs import *
class TestInterpreter(unittest.TestCase):
@@ -40,3 +41,15 @@ def g():
self.assertEquals(str(plain('').join(a)), str(expected))
self.assertEquals(plain('').join(a), expected)
+
+class TestPreprocessing(unittest.TestCase):
+ def test_bad_empty_lines_removed(self):
+ self.assertEqual(bad_empty_lines_removed("def foo():\n"
+ " return 1\n"
+ "\n"
+ " pass\n"),
+ "def foo():\n"
+ " return 1\n"
+ " pass\n")
+
+
From c2b4fbe46c70c5c1fe85f61502b70d77bc8414ca Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Sun, 23 Nov 2014 00:10:40 -0500
Subject: [PATCH 0112/1650] fix travis build
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 7c3c9c3a1..fbb52c98d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,7 @@ python:
install:
- "python setup.py install"
- - "pip install curtsies greenlet pygments requests"
+ - "pip install 'curtsies<0.2.0' greenlet pygments requests"
script:
- cd build/lib/
From 57d4ee0c72e0551fdec7c82ebe5900eb128bab60 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Fri, 12 Dec 2014 22:50:08 +0100
Subject: [PATCH 0113/1650] Sprinkle some assertIn/assertNotIn into tests. Refs
#428.
---
.travis.yml | 2 +-
bpython/test/test_crashers.py | 11 ++++-----
bpython/test/test_manual_readline.py | 6 ++---
bpython/test/test_repl.py | 34 +++++++++++++---------------
setup.py | 2 +-
5 files changed, 25 insertions(+), 30 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index fbb52c98d..d45d69954 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,7 @@ python:
install:
- "python setup.py install"
- - "pip install 'curtsies<0.2.0' greenlet pygments requests"
+ - "pip install 'curtsies<0.2.0' greenlet pygments requests unittest2"
script:
- cd build/lib/
diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py
index 533378ca1..a28fdf134 100644
--- a/bpython/test/test_crashers.py
+++ b/bpython/test/test_crashers.py
@@ -5,13 +5,11 @@
import sys
import termios
import textwrap
-import unittest
try:
- from unittest import skip
+ import unittest2 as unittest
except ImportError:
- def skip(f):
- return lambda self: None
+ import unittest
try:
from twisted.internet import reactor
@@ -103,14 +101,13 @@ def spam(a, (b, c)):
return self.run_bpython(input).addCallback(self.check_no_traceback)
def check_no_traceback(self, data):
- tb = data[data.find("Traceback"):]
- self.assertTrue("Traceback" not in data, tb)
+ self.assertNotIn("Traceback", data)
if reactor is not None:
class CursesCrashersTest(TrialTestCase, CrashersTest):
backend = "cli"
- @skip("take 6 seconds, and Simon says we can skip them")
+ @unittest.skip("take 6 seconds, and Simon says we can skip them")
class UrwidCrashersTest(TrialTestCase, CrashersTest):
backend = "urwid"
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index 5cfdcc6c7..f0bbf4719 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -222,7 +222,7 @@ def setUp(self):
def test_seq(self):
f = lambda cursor_offset, line: ('hi', 2)
self.edits.add('a', f)
- self.assertTrue('a' in self.edits)
+ self.assertIn('a', self.edits)
self.assertEqual(self.edits['a'], f)
self.assertEqual(self.edits.call('a', cursor_offset=3, line='hello'),
('hi', 2))
@@ -245,12 +245,12 @@ def test_config(self):
f = lambda cursor_offset, line: ('hi', 2)
g = lambda cursor_offset, line: ('hey', 3)
self.edits.add_config_attr('att', f)
- self.assertFalse('att' in self.edits)
+ self.assertNotIn('att', self.edits)
class config: att = 'c'
key_dispatch = {'c': 'c'}
configured_edits = self.edits.mapping_with_config(config, key_dispatch)
self.assertTrue(configured_edits.__contains__, 'c')
- self.assertFalse('c' in self.edits)
+ self.assertNotIn('c', self.edits)
self.assertRaises(NotImplementedError,
configured_edits.add_config_attr, 'att2', g)
self.assertRaises(NotImplementedError,
diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py
index 7eaa5315d..3b246f29a 100644
--- a/bpython/test/test_repl.py
+++ b/bpython/test/test_repl.py
@@ -3,15 +3,13 @@
import os
import socket
import sys
-import unittest
from mock import Mock, MagicMock
try:
- from unittest import skip
+ import unittest2 as unittest
except ImportError:
- def skip(f):
- return lambda self: None
+ import unittest
py3 = (sys.version_info[0] == 3)
@@ -101,7 +99,7 @@ def test_append(self):
self.assertEqual(self.history.back(), 'print "foo\n"')
- @skip("I don't understand this test")
+ @unittest.skip("I don't understand this test")
def test_enter(self):
self.history.enter('#lastnumber!')
@@ -278,7 +276,7 @@ def assert_get_source_error_for_current_function(self, func, msg):
def test_current_function(self):
self.set_input_line('INPUTLINE')
self.repl.current_func = collections.MutableSet.add
- self.assertTrue("Add an element." in self.repl.get_source_of_current_name())
+ self.assertIn("Add an element.", self.repl.get_source_of_current_name())
self.assert_get_source_error_for_current_function(
collections.defaultdict.copy, "No source code found for INPUTLINE")
@@ -295,7 +293,7 @@ def test_current_function(self):
def test_current_line(self):
self.repl.interp.locals['a'] = socket.socket
self.set_input_line('a')
- self.assertTrue('dup(self)' in self.repl.get_source_of_current_name())
+ self.assertIn('dup(self)', self.repl.get_source_of_current_name())
#TODO add tests for various failures without using current function
@@ -334,7 +332,7 @@ def test_simple_global_complete(self):
self.assertEqual(self.repl.matches_iter.matches,
['def', 'del', 'delattr(', 'dict(', 'dir(', 'divmod('])
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_substring_global_complete(self):
self.repl = FakeRepl({'autocomplete_mode': autocomplete.SUBSTRING})
self.setInputLine("time")
@@ -344,7 +342,7 @@ def test_substring_global_complete(self):
self.assertEqual(self.repl.completer.matches,
['RuntimeError(', 'RuntimeWarning('])
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_fuzzy_global_complete(self):
self.repl = FakeRepl({'autocomplete_mode': autocomplete.FUZZY})
self.setInputLine("doc")
@@ -368,7 +366,7 @@ def test_simple_attribute_complete(self):
self.assertEqual(self.repl.matches_iter.matches,
['Foo.bar'])
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_substring_attribute_complete(self):
self.repl = FakeRepl({'autocomplete_mode': autocomplete.SUBSTRING})
self.setInputLine("Foo.az")
@@ -382,7 +380,7 @@ def test_substring_attribute_complete(self):
self.assertEqual(self.repl.completer.matches,
['Foo.baz'])
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_fuzzy_attribute_complete(self):
self.repl = FakeRepl({'autocomplete_mode': autocomplete.FUZZY})
self.setInputLine("Foo.br")
@@ -412,7 +410,7 @@ def test_file_should_not_appear_in_complete(self):
self.setInputLine("_")
self.assertTrue(self.repl.complete())
self.assertTrue(hasattr(self.repl.matches_iter,'matches'))
- self.assertTrue('__file__' not in self.repl.matches_iter.matches)
+ self.assertNotIn('__file__', self.repl.matches_iter.matches)
class TestCliRepl(unittest.TestCase):
@@ -465,7 +463,7 @@ def test_simple_tab_complete(self):
self.repl.complete.assert_called_with(tab=True)
self.assertEqual(self.repl.s, "foobar")
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_substring_tab_complete(self):
self.repl.s = "bar"
self.repl.config.autocomplete_mode = autocomplete.FUZZY
@@ -474,7 +472,7 @@ def test_substring_tab_complete(self):
self.repl.tab()
self.assertEqual(self.repl.s, "foofoobar")
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_fuzzy_tab_complete(self):
self.repl.s = "br"
self.repl.config.autocomplete_mode = autocomplete.FUZZY
@@ -509,7 +507,7 @@ def test_back_parameter(self):
self.assertTrue(self.repl.s, "previtem")
# Attribute Tests
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_fuzzy_attribute_tab_complete(self):
"""Test fuzzy attribute with no text"""
self.repl.s = "Foo."
@@ -518,7 +516,7 @@ def test_fuzzy_attribute_tab_complete(self):
self.repl.tab()
self.assertEqual(self.repl.s, "Foo.foobar")
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_fuzzy_attribute_tab_complete2(self):
"""Test fuzzy attribute with some text"""
self.repl.s = "Foo.br"
@@ -538,14 +536,14 @@ def test_simple_expand(self):
self.repl.tab()
self.assertEqual(self.repl.s, "foo")
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_substring_expand_forward(self):
self.repl.config.autocomplete_mode = autocomplete.SUBSTRING
self.repl.s = "ba"
self.repl.tab()
self.assertEqual(self.repl.s, "bar")
- @skip("disabled while non-simple completion is disabled")
+ @unittest.skip("disabled while non-simple completion is disabled")
def test_fuzzy_expand(self):
pass
diff --git a/setup.py b/setup.py
index 9e67146fe..936974275 100755
--- a/setup.py
+++ b/setup.py
@@ -183,7 +183,7 @@ def initialize_options(self):
'requests'
],
extras_require = extras_require,
- tests_require = ['mock'],
+ tests_require = ['mock', 'unittest2'],
packages = packages,
data_files = data_files,
package_data = {
From 6966e2e20643f3712681bc8742347cb213fb1bbc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20St=C3=BChrk?=
Date: Fri, 12 Dec 2014 23:06:23 +0100
Subject: [PATCH 0114/1650] Add missing unittest2 import.
---
bpython/test/test_manual_readline.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py
index f0bbf4719..58b6550e8 100644
--- a/bpython/test/test_manual_readline.py
+++ b/bpython/test/test_manual_readline.py
@@ -1,5 +1,10 @@
+try:
+ import unittest2 as unittest
+except ImportError:
+ import unittest
+
from bpython.curtsiesfrontend.manual_readline import *
-import unittest
+
class TestManualReadline(unittest.TestCase):
def setUp(self):
From f20d6d56ce3513fb5763d939943fe5565e4102cb Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 23 Dec 2014 15:24:49 +0100
Subject: [PATCH 0115/1650] Depend on PyOpenSSL for SNI support (fixes #430)
This is only required for Python 2.X on Mac OS X.
Signed-off-by: Sebastian Ramacher
---
setup.py | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/setup.py b/setup.py
index 936974275..ac6c2cb20 100755
--- a/setup.py
+++ b/setup.py
@@ -135,6 +135,11 @@ def initialize_options(self):
]
data_files.extend(man_pages)
+install_requires = [
+ 'pygments',
+ 'requests'
+]
+
extras_require = {
'urwid' : ['urwid']
}
@@ -161,6 +166,10 @@ def initialize_options(self):
if not using_setuptools:
scripts.append('data/bpython-curtsies')
+if sys.version_info[0] == 2 and sys.platform == "darwin":
+ # need PyOpenSSL for SNI support (only 2.X and on Darwin)
+ install_requires.append('PyOpenSSL')
+
# translations
mo_files = list()
for language in os.listdir(translations_dir):
@@ -178,10 +187,7 @@ def initialize_options(self):
url = "http://www.bpython-interpreter.org/",
long_description = """bpython is a fancy interface to the Python
interpreter for Unix-like operating systems.""",
- install_requires = [
- 'pygments',
- 'requests'
- ],
+ install_requires = install_requires,
extras_require = extras_require,
tests_require = ['mock', 'unittest2'],
packages = packages,
From f977d810274916c07aa0d207e870041c3f459dae Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 23 Dec 2014 15:33:40 +0100
Subject: [PATCH 0116/1650] Also depend on ndg-httpsclient and pyasn1
Signed-off-by: Sebastian Ramacher
---
setup.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/setup.py b/setup.py
index ac6c2cb20..f86cb1f8a 100755
--- a/setup.py
+++ b/setup.py
@@ -168,7 +168,11 @@ def initialize_options(self):
if sys.version_info[0] == 2 and sys.platform == "darwin":
# need PyOpenSSL for SNI support (only 2.X and on Darwin)
+ # list of packages taken from
+ # https://github.com/kennethreitz/requests/blob/master/requests/packages/urllib3/contrib/pyopenssl.py
install_requires.append('PyOpenSSL')
+ install_requires.append('ndg-httpsclient')
+ install_requires.append('pyasn1')
# translations
mo_files = list()
From 4091bdc5f058b1c162f5e9fcbed0f21e0e368bf9 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 20:19:46 +0100
Subject: [PATCH 0117/1650] Install dependencies first
Signed-off-by: Sebastian Ramacher
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index fbb52c98d..afe051f82 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,8 +7,8 @@ python:
- "3.4"
install:
- - "python setup.py install"
- "pip install 'curtsies<0.2.0' greenlet pygments requests"
+ - "python setup.py install"
script:
- cd build/lib/
From 97c2b067cce8b43ed033e454ba086bbead09066d Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 20:33:12 +0100
Subject: [PATCH 0118/1650] Install more dependencies
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
… and comment why they are needed. This enables building of the documentation
and translations.
Signed-off-by: Sebastian Ramacher
---
.travis.yml | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index afe051f82..cf2c45c3c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,11 @@ python:
- "3.4"
install:
- - "pip install 'curtsies<0.2.0' greenlet pygments requests"
+ - "pip install setuptools"
+ - "pip install pygments requests" # core dependencies
+ - "pip install 'curtsies<0.2.0' greenlet" # curtsies specific dependencies
+ - "pip install babel" # translation specific dependencies
+ - "pip install sphinx" # documentation specific dependencies
- "python setup.py install"
script:
From 0db42ff67b0e8eb5dcd878875f5ca065891a4c6e Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 6 Jan 2015 14:33:34 -0500
Subject: [PATCH 0119/1650] document get_completer autocompletion helper
---
bpython/autocomplete.py | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index 914eef150..000b9fcdf 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -60,15 +60,26 @@
def after_last_dot(name):
return name.rstrip('.').rsplit('.')[-1]
-def get_completer(cursor_offset, current_line, locals_, argspec, full_code, mode, complete_magic_methods):
+def get_completer(cursor_offset, current_line, locals_, argspec, current_block,
+ mode, complete_magic_methods):
"""Returns a list of matches and a class for what kind of completion is happening
If no completion type is relevant, returns None, None
- argspec is an output of inspect.getargspec
+ Params:
+ cursor_offset is the current cursor column
+ current_line is a string of the current line
+ locals_ is a dictionary of the environment
+ argspec is an inspect.ArgSpec instance for the current function where
+ the cursor is
+ current_block is the possibly multiline not-yet-evaluated block of
+ code which the current line is part of
+ mode is one of SIMPLE, SUBSTRING or FUZZY - ways to find matches
+ complete_magic_methods is a bool of whether we ought to complete
+ double underscore methods like __len__ in method signatures
"""
- kwargs = {'locals_':locals_, 'argspec':argspec, 'full_code':full_code,
+ kwargs = {'locals_':locals_, 'argspec':argspec, 'current_block':current_block,
'mode':mode, 'complete_magic_methods':complete_magic_methods}
# mutually exclusive if matches: If one of these returns [], try the next one
@@ -220,11 +231,11 @@ def format(cls, match):
class MagicMethodCompletion(BaseCompletionType):
locate = staticmethod(lineparts.current_method_definition_name)
@classmethod
- def matches(cls, cursor_offset, line, full_code, **kwargs):
+ def matches(cls, cursor_offset, line, current_block, **kwargs):
r = cls.locate(cursor_offset, line)
if r is None:
return None
- if 'class' not in full_code:
+ if 'class' not in current_block:
return None
start, end, word = r
return [name for name in MAGIC_METHODS if name.startswith(word)]
From 9119e5355438e97a1c4da1f03e3e5eee60927c6a Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 20:37:37 +0100
Subject: [PATCH 0120/1650] Install unittests2 for Python 2.6
Signed-off-by: Sebastian Ramacher
---
.travis.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.travis.yml b/.travis.yml
index cf2c45c3c..b5f6395e8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -12,6 +12,7 @@ install:
- "pip install 'curtsies<0.2.0' greenlet" # curtsies specific dependencies
- "pip install babel" # translation specific dependencies
- "pip install sphinx" # documentation specific dependencies
+ - if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install unittest2; fi
- "python setup.py install"
script:
From ab54341086c6335fe816ce9576c74174d00e15bc Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 20:38:26 +0100
Subject: [PATCH 0121/1650] Reformat
---
.travis.yml | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index b5f6395e8..874278737 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,13 +7,17 @@ python:
- "3.4"
install:
- - "pip install setuptools"
- - "pip install pygments requests" # core dependencies
- - "pip install 'curtsies<0.2.0' greenlet" # curtsies specific dependencies
- - "pip install babel" # translation specific dependencies
- - "pip install sphinx" # documentation specific dependencies
+ - pip install setuptools
+ # core dependencies
+ - pip install pygments requests
+ # curtsies specific dependencies
+ - pip install 'curtsies<0.2.0' greenlet
+ # translation specific dependencies
+ - pip install babel
+ # documentation specific dependencies
+ - pip install sphinx
- if [[ $TRAVIS_PYTHON_VERSION == 2.6 ]]; then pip install unittest2; fi
- - "python setup.py install"
+ - python setup.py install
script:
- cd build/lib/
From cd8d7344a8480b2a5b0afaccec9d2177c67aa654 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 20:43:58 +0100
Subject: [PATCH 0122/1650] Fix title underline
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/community.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/sphinx/source/community.rst b/doc/sphinx/source/community.rst
index 2b946a48d..2d1be5c98 100644
--- a/doc/sphinx/source/community.rst
+++ b/doc/sphinx/source/community.rst
@@ -16,7 +16,7 @@ from Europe and when you get to the channel during our nighttime you might have
to wait a while for a response.
Mailing List
------------
+------------
We have a mailing list at `google groups `_.
You can post questions there and releases are announced on the mailing
list.
From d6de41b9506fca3424337e23965295cbff1d6a38 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 20:58:43 +0100
Subject: [PATCH 0123/1650] Only depend on unittest2 for Python 2.X with X < 7
Signed-off-by: Sebastian Ramacher
---
setup.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index f86cb1f8a..cfa181e00 100755
--- a/setup.py
+++ b/setup.py
@@ -174,6 +174,10 @@ def initialize_options(self):
install_requires.append('ndg-httpsclient')
install_requires.append('pyasn1')
+tests_require = ['mock']
+if sys.version_info[0] == 2 and sys.version_info[1] < 7:
+ tests_require.append('unittest2')
+
# translations
mo_files = list()
for language in os.listdir(translations_dir):
@@ -193,7 +197,7 @@ def initialize_options(self):
interpreter for Unix-like operating systems.""",
install_requires = install_requires,
extras_require = extras_require,
- tests_require = ['mock', 'unittest2'],
+ tests_require = tests_require,
packages = packages,
data_files = data_files,
package_data = {
From a29140bb5cebdf8dfd230f8d16c96599a2f6d191 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 21:09:08 +0100
Subject: [PATCH 0124/1650] setuptools only setup.py
Signed-off-by: Sebastian Ramacher
---
setup.py | 24 +++++-------------------
1 file changed, 5 insertions(+), 19 deletions(-)
diff --git a/setup.py b/setup.py
index cfa181e00..8fcb35657 100755
--- a/setup.py
+++ b/setup.py
@@ -7,22 +7,14 @@
import sys
from distutils.command.build import build
-
-from bpython import __version__, package_dir
-
+from setuptools import setup
+from setuptools.command.install import install as _install
try:
- from setuptools import setup
- from setuptools.command.install import install as _install
- using_setuptools = True
+ from setuptools.command.build_py import build_py_2to3 as build_py
except ImportError:
- from distutils.core import setup
- from distutils.command.install import install as _install
- using_setuptools = False
+ from setuptools.command.build_py import build_py
-try:
- from distutils.command.build_py import build_py_2to3 as build_py
-except ImportError:
- from distutils.command.build_py import build_py
+from bpython import __version__, package_dir
try:
from babel.messages.frontend import compile_catalog as _compile_catalog
@@ -153,9 +145,6 @@ def initialize_options(self):
]
}
-scripts = [] if using_setuptools else ['data/bpython',
- 'data/bpython-urwid']
-
if sys.version_info[:2] >= (2, 6):
# curtsies only supports 2.6 and onwards
extras_require['curtsies'] = ['curtsies >=0.1.15, <0.2.0', 'greenlet']
@@ -163,8 +152,6 @@ def initialize_options(self):
packages.append("bpython.curtsiesfrontend")
entry_points['console_scripts'].append(
'bpython-curtsies = bpython.curtsies:main [curtsies]')
- if not using_setuptools:
- scripts.append('data/bpython-curtsies')
if sys.version_info[0] == 2 and sys.platform == "darwin":
# need PyOpenSSL for SNI support (only 2.X and on Darwin)
@@ -206,7 +193,6 @@ def initialize_options(self):
'bpython.test': ['test.config', 'test.theme']
},
entry_points = entry_points,
- scripts = scripts,
cmdclass = cmdclass,
test_suite = 'bpython.test'
)
From 12a31f622422ce8931265cc5784b0ab3f29dd6ed Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 21:20:53 +0100
Subject: [PATCH 0125/1650] Clean up scripts in data
Signed-off-by: Sebastian Ramacher
---
data/bpython | 6 ------
data/bpython-curtsies | 6 ------
data/bpython-urwid | 6 ------
3 files changed, 18 deletions(-)
delete mode 100755 data/bpython
delete mode 100755 data/bpython-curtsies
delete mode 100755 data/bpython-urwid
diff --git a/data/bpython b/data/bpython
deleted file mode 100755
index 780e7a0de..000000000
--- a/data/bpython
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-from bpython.cli import main
-
-sys.exit(main())
diff --git a/data/bpython-curtsies b/data/bpython-curtsies
deleted file mode 100755
index ac4ced087..000000000
--- a/data/bpython-curtsies
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-from bpython.curtsies import main
-
-sys.exit(main())
diff --git a/data/bpython-urwid b/data/bpython-urwid
deleted file mode 100755
index 3d8f01b3e..000000000
--- a/data/bpython-urwid
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-from bpython.urwid import main
-
-sys.exit(main())
From 2d34994214514d6a721ca702a5c480cd864f7b2d Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Tue, 6 Jan 2015 15:24:03 -0500
Subject: [PATCH 0126/1650] only call update_completion once when adding a
character
---
bpython/curtsiesfrontend/repl.py | 14 ++++++++------
bpython/repl.py | 2 +-
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 3735d7080..c31eba82f 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -753,9 +753,12 @@ def add_normal_character(self, char):
if self.incremental_search_mode:
self.add_to_incremental_search(char)
else:
- self.current_line = (self.current_line[:self.cursor_offset] +
- char +
- self.current_line[self.cursor_offset:])
+ self._set_current_line((self.current_line[:self.cursor_offset] +
+ char +
+ self.current_line[self.cursor_offset:]),
+ update_completion=False,
+ reset_rl_history=False,
+ clear_special_mode=False)
self.cursor_offset += 1
if self.config.cli_trim_prompts and self.current_line.startswith(self.ps1):
self.current_line = self.current_line[4:]
@@ -1232,14 +1235,13 @@ def _get_cursor_offset(self):
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=False, clear_special_mode=True):
if self._cursor_offset == offset:
return
- if update_completion:
- self.update_completion()
if reset_rl_history:
self.rl_history.reset()
if clear_special_mode:
self.incremental_search_mode = None
self._cursor_offset = offset
- self.update_completion()
+ if update_completion:
+ self.update_completion()
self.unhighlight_paren()
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,
"The current cursor offset from the front of the line")
diff --git a/bpython/repl.py b/bpython/repl.py
index b767189d7..81900aec9 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -629,7 +629,7 @@ def set_docstring(self):
self.docstring = None
def complete(self, tab=False):
- """Construct a full list of possible completions and construct and
+ """Construct a full list of possible completions and
display them in a window. Also check if there's an available argspec
(via the inspect module) and bang that on top of the completions too.
The return value is whether the list_win is visible or not.
From 8c11b561e2a5f44d085cf93b6f1c2b950d97920f Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Tue, 6 Jan 2015 21:37:34 +0100
Subject: [PATCH 0127/1650] Just use use_2to3 instead of playing with commands
Signed-off-by: Sebastian Ramacher
---
setup.py | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/setup.py b/setup.py
index 8fcb35657..1818471ae 100755
--- a/setup.py
+++ b/setup.py
@@ -9,10 +9,6 @@
from distutils.command.build import build
from setuptools import setup
from setuptools.command.install import install as _install
-try:
- from setuptools.command.build_py import build_py_2to3 as build_py
-except ImportError:
- from setuptools.command.build_py import build_py
from bpython import __version__, package_dir
@@ -44,7 +40,7 @@ def run(self):
self.run_command('build')
_install.run(self)
-cmdclass = dict(build_py=build_py, build=build, install=install)
+cmdclass = dict(build=build, install=install)
translations_dir = os.path.join(package_dir, 'translations')
# localization options
@@ -194,7 +190,8 @@ def initialize_options(self):
},
entry_points = entry_points,
cmdclass = cmdclass,
- test_suite = 'bpython.test'
+ test_suite = 'bpython.test',
+ use_2to3 = True
)
# vim: fileencoding=utf-8 sw=4 ts=4 sts=4 ai et sta
From d441dd54b4d383c4a469aed3322f7184fdc7af57 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 02:56:04 +0100
Subject: [PATCH 0128/1650] Move curtsies.bpythonparse to
bpython.curtsiesfrontent.parse
This is the bpython part of curtsies #44.
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/interpreter.py | 4 +-
bpython/curtsiesfrontend/parse.py | 90 +++++++++++++++++++++++++
bpython/curtsiesfrontend/repl.py | 4 +-
bpython/curtsiesfrontend/replpainter.py | 2 +-
bpython/test/test_curtsies_parser.py | 18 +++++
5 files changed, 113 insertions(+), 5 deletions(-)
create mode 100644 bpython/curtsiesfrontend/parse.py
create mode 100644 bpython/test/test_curtsies_parser.py
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index a030ea12a..a980079c7 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -3,7 +3,7 @@
import sys
from pygments.token import *
from pygments.formatter import Formatter
-from curtsies.bpythonparse import parse
+from bpython.curtsiesfrontend.parse import parse
from codeop import CommandCompiler
from pygments.lexers import get_lexer_by_name
@@ -27,7 +27,7 @@
Name.Function:'d',
Name.Class:'d',
}
-
+
class BPythonFormatter(Formatter):
"""This is subclassed from the custom formatter for bpython.
Its format() method receives the tokensource
diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py
new file mode 100644
index 000000000..37f80b33e
--- /dev/null
+++ b/bpython/curtsiesfrontend/parse.py
@@ -0,0 +1,90 @@
+
+from bpython.formatter import BPythonFormatter
+from bpython._py3compat import PythonLexer
+from bpython.config import Struct, loadini, default_config_path
+
+from curtsies.termformatconstants import FG_COLORS, BG_COLORS, colors
+from curtsies.formatstring import fmtstr, FmtStr
+
+from pygments import format
+from functools import partial
+
+import re
+
+cnames = dict(list(zip('krgybmcwd', colors + ('default',))))
+
+def func_for_letter(l, default='k'):
+ """Returns FmtStr constructor for a bpython-style color code"""
+ if l == 'd':
+ l = default
+ elif l == 'D':
+ l = default.upper()
+ return partial(fmtstr, fg=cnames[l.lower()], bold=(l.lower() != l))
+
+def color_for_letter(l, default='k'):
+ if l == 'd':
+ l = default
+ return cnames[l.lower()]
+
+def parse(s):
+ """Returns a FmtStr object from a bpython-formatted colored string"""
+ rest = s
+ stuff = []
+ while True:
+ if not rest:
+ break
+ d, rest = peel_off_string(rest)
+ stuff.append(d)
+ return (sum([fs_from_match(d) for d in stuff[1:]], fs_from_match(stuff[0]))
+ if len(stuff) > 0
+ else FmtStr())
+
+def fs_from_match(d):
+ atts = {}
+ if d['fg']:
+
+ # this isn't according to spec as I understand it
+ if d['fg'] != d['fg'].lower():
+ d['bold'] = True
+ #TODO figure out why boldness isn't based on presence of \x02
+
+ color = cnames[d['fg'].lower()]
+ if color != 'default':
+ atts['fg'] = FG_COLORS[color]
+ if d['bg']:
+ if d['bg'] == 'I':
+ color = colors[(colors.index(color) + (len(colors) // 2)) % len(colors)] # hack for finding the "inverse"
+ else:
+ color = cnames[d['bg'].lower()]
+ if color != 'default':
+ atts['bg'] = BG_COLORS[color]
+ if d['bold']:
+ atts['bold'] = True
+ return fmtstr(d['string'], **atts)
+
+def peel_off_string(s):
+ r"""
+ >>> r = peel_off_string('\x01RI\x03]\x04asdf')
+ >>> r == ({'bg': 'I', 'string': ']', 'fg': 'R', 'colormarker': '\x01RI', 'bold': ''}, 'asdf')
+ True
+ """
+ p = r"""(?P\x01
+ (?P[krgybmcwdKRGYBMCWD]?)
+ (?P[krgybmcwdKRGYBMCWDI]?)?)
+ (?P\x02?)
+ \x03
+ (?P[^\x04]*)
+ \x04
+ (?P.*)
+ """
+ m = re.match(p, s, re.VERBOSE | re.DOTALL)
+ assert m, repr(s)
+ d = m.groupdict()
+ rest = d['rest']
+ del d['rest']
+ return d, rest
+
+def string_to_fmtstr(x):
+ config = Struct()
+ loadini(config, default_config_path())
+ return parse(format(PythonLexer().get_tokens(x), BPythonFormatter(config.color_scheme)))
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 3735d7080..7eed90c81 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -23,8 +23,6 @@
import curtsies
from curtsies import FSArray, fmtstr, FmtStr, Termmode
-from curtsies.bpythonparse import parse as bpythonparse
-from curtsies.bpythonparse import func_for_letter, color_for_letter
from curtsies import fmtfuncs
from curtsies import events
@@ -44,6 +42,8 @@
from bpython.curtsiesfrontend.interaction import StatusBar
from bpython.curtsiesfrontend.manual_readline import edit_keys
from bpython.curtsiesfrontend import events as bpythonevents
+from bpython.curtsiesfrontend.parse import parse as bpythonparse
+from bpython.curtsiesfrontend.parse import func_for_letter, color_for_letter
#TODO other autocomplete modes (also fix in other bpython implementations)
diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py
index e24467657..b5141cefa 100644
--- a/bpython/curtsiesfrontend/replpainter.py
+++ b/bpython/curtsiesfrontend/replpainter.py
@@ -2,10 +2,10 @@
import logging
from curtsies import fsarray, fmtstr
-from curtsies.bpythonparse import func_for_letter
from curtsies.formatstring import linesplit
from curtsies.fmtfuncs import bold
+from bpython.curtsiesfrontend.parse import func_for_letter
from bpython._py3compat import py3
if not py3:
import inspect
diff --git a/bpython/test/test_curtsies_parser.py b/bpython/test/test_curtsies_parser.py
new file mode 100644
index 000000000..09cae32b3
--- /dev/null
+++ b/bpython/test/test_curtsies_parser.py
@@ -0,0 +1,18 @@
+import unittest
+
+from bpython.curtsiesfrontend import parse
+from curtsies.fmtfuncs import yellow, cyan, green, bold
+
+class TestExecArgs(unittest.TestCase):
+
+ def test_parse(self):
+ self.assertEquals(parse.parse(u'\x01y\x03print\x04'),
+ yellow(u'print'))
+
+ self.assertEquals(parse.parse(u'\x01y\x03print\x04\x01c\x03 \x04\x01g\x031\x04\x01c\x03 \x04\x01Y\x03+\x04\x01c\x03 \x04\x01g\x032\x04'),
+ yellow(u'print')+cyan(u' ')+green(u'1')+cyan(u' ')+bold(yellow(u'+'))+cyan(' ')+green(u'2'))
+
+ def test_peal_off_string(self):
+ self.assertEquals(parse.peel_off_string('\x01RI\x03]\x04asdf'),
+ ({'bg': 'I', 'string': ']', 'fg': 'R', 'colormarker':
+ '\x01RI', 'bold': ''}, 'asdf'))
From 3afa1db8e4527c39abaf4195bc692c90f92d70cd Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 07:37:40 +0100
Subject: [PATCH 0129/1650] Expand history file path only once
Signed-off-by: Sebastian Ramacher
---
bpython/config.py | 2 ++
bpython/repl.py | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/bpython/config.py b/bpython/config.py
index a6f0e3569..d804a6d03 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -229,6 +229,8 @@ def get_key_no_doublebind(attr, already_used={}):
for key in (struct.pastebin_key, struct.save_key):
key_dispatch[key]
+ struct.hist_file = os.path.expanduser(struct.hist_file)
+
def load_theme(struct, path, colors, default_colors):
theme = ConfigParser()
with open(path, 'r') as f:
diff --git a/bpython/repl.py b/bpython/repl.py
index b767189d7..a19d7fbcc 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -890,7 +890,7 @@ def push(self, s, insert_into_history=True):
def insert_into_history(self, s):
if self.config.hist_length:
- histfilename = os.path.expanduser(self.config.hist_file)
+ histfilename = self.config.hist_file
oldhistory = self.rl_history.entries
self.rl_history.entries = []
if os.path.exists(histfilename):
From 4cd6684e786e2a677a9fa47e5b61d2071763ee03 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 07:47:57 +0100
Subject: [PATCH 0130/1650] Remove unnecessary close
Signed-off-by: Sebastian Ramacher
---
bpython/config.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/bpython/config.py b/bpython/config.py
index d804a6d03..274cd1f60 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -245,4 +245,3 @@ def load_theme(struct, path, colors, default_colors):
for k, v in default_colors.iteritems():
if k not in colors:
colors[k] = v
- f.close()
From 65a17cf8420c02b617c438884ef7a59834e32031 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:12:18 +0100
Subject: [PATCH 0131/1650] Use except Foo as e consistently
Signed-off-by: Sebastian Ramacher
---
bpython/cli.py | 4 ++--
bpython/curtsies.py | 2 +-
bpython/curtsiesfrontend/repl.py | 2 +-
bpython/pager.py | 8 +++++---
bpython/repl.py | 10 +++++-----
5 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index 46db9d7e1..ac7075e60 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -1077,7 +1077,7 @@ def push(self, s, insert_into_history=True):
curses.raw(False)
try:
return repl.Repl.push(self, s, insert_into_history)
- except SystemExit, e:
+ except SystemExit as e:
# Avoid a traceback on e.g. quit()
self.do_exit = True
self.exit_value = e.args
@@ -1895,7 +1895,7 @@ def main_curses(scr, args, config, interactive=True, locals_=None,
exit_value = ()
try:
bpython.args.exec_code(interpreter, args)
- except SystemExit, e:
+ except SystemExit as e:
# The documentation of code.InteractiveInterpreter.runcode claims
# that it reraises SystemExit. However, I can't manage to trigger
# that. To be one the safe side let's catch SystemExit here anyway.
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index d0282b92f..e98b2fd2b 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -58,7 +58,7 @@ def main(args=None, locals_=None, banner=None):
try:
interp = code.InteractiveInterpreter(locals=locals_)
bpargs.exec_code(interp, exec_args)
- except SystemExit, e:
+ except SystemExit as e:
exit_value = e.args
if not options.interactive:
raise SystemExit(exit_value)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 7eed90c81..ee1b55227 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1351,7 +1351,7 @@ def pager(self, text):
def show_source(self):
try:
source = self.get_source_of_current_name()
- except SourceNotFound, e:
+ except SourceNotFound as e:
self.status_bar.message(_(e))
else:
if self.config.highlight_show_source:
diff --git a/bpython/pager.py b/bpython/pager.py
index 73cc8f5f5..b397b2827 100644
--- a/bpython/pager.py
+++ b/bpython/pager.py
@@ -54,20 +54,22 @@ def page(data, use_internal=False):
data = data.encode(sys.__stdout__.encoding, 'replace')
popen.stdin.write(data)
popen.stdin.close()
- except OSError, e:
+ except OSError as e:
if e.errno == errno.ENOENT:
# pager command not found, fall back to internal pager
page_internal(data)
return
- except IOError, e:
+ except IOError as e:
if e.errno != errno.EPIPE:
raise
while True:
try:
popen.wait()
- except OSError, e:
+ except OSError as e:
if e.errno != errno.EINTR:
raise
else:
break
curses.doupdate()
+
+# vim: sw=4 ts=4 sts=4 ai et
diff --git a/bpython/repl.py b/bpython/repl.py
index a19d7fbcc..8c016434d 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -602,11 +602,11 @@ def get_source_of_current_name(self):
if inspection.is_eval_safe_name(line):
obj = self.get_object(line)
return inspect.getsource(obj)
- except (AttributeError, NameError), e:
+ except (AttributeError, NameError) as e:
msg = "Cannot get source: " + str(e)
- except IOError, e:
+ except IOError as e:
msg = str(e)
- except TypeError, e:
+ except TypeError as e:
if "built-in" in str(e):
msg = "Cannot access source of %r" % (obj, )
else:
@@ -840,7 +840,7 @@ def do_pastebin_helper(self, s):
helper.stdin.write(s.encode(getpreferredencoding()))
output = helper.communicate()[0].decode(getpreferredencoding())
paste_url = output.split()[0]
- except OSError, e:
+ except OSError as e:
if e.errno == errno.ENOENT:
self.interact.notify(_('Upload failed: '
'Helper program not found.'))
@@ -898,7 +898,7 @@ def insert_into_history(self, s):
self.rl_history.append(s)
try:
self.rl_history.save(histfilename, getpreferredencoding(), self.config.hist_length)
- except EnvironmentError, err:
+ except EnvironmentError as err:
self.interact.notify("Error occured while writing to file %s (%s) " % (histfilename, err.strerror))
self.rl_history.entries = oldhistory
self.rl_history.append(s)
From 99a889b47a48f0e9dbf9d74f81cf185540c8caff Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:12:36 +0100
Subject: [PATCH 0132/1650] Remove unnecessary import
Signed-off-by: Sebastian Ramacher
---
bpython/cli.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index ac7075e60..f916aba64 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -1957,7 +1957,6 @@ def main(args=None, locals_=None, banner=None):
return repl.extract_exit_value(exit_value)
if __name__ == '__main__':
- from bpython.cli import main
sys.exit(main())
# vim: sw=4 ts=4 sts=4 ai et
From 9c43cdd8b6526d4b98ecf4fcf426167ced7faed4 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:16:53 +0100
Subject: [PATCH 0133/1650] Tests are now in bpython.tests
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/parse.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py
index 37f80b33e..5c089cc2e 100644
--- a/bpython/curtsiesfrontend/parse.py
+++ b/bpython/curtsiesfrontend/parse.py
@@ -63,11 +63,6 @@ def fs_from_match(d):
return fmtstr(d['string'], **atts)
def peel_off_string(s):
- r"""
- >>> r = peel_off_string('\x01RI\x03]\x04asdf')
- >>> r == ({'bg': 'I', 'string': ']', 'fg': 'R', 'colormarker': '\x01RI', 'bold': ''}, 'asdf')
- True
- """
p = r"""(?P\x01
(?P[krgybmcwdKRGYBMCWD]?)
(?P[krgybmcwdKRGYBMCWDI]?)?)
From d9274182c9eba0925b8d6480f6b9e5b61c3af148 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:17:11 +0100
Subject: [PATCH 0134/1650] Fix a pyflakes warning
---
bpython/curtsiesfrontend/interpreter.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py
index a980079c7..850674a85 100644
--- a/bpython/curtsiesfrontend/interpreter.py
+++ b/bpython/curtsiesfrontend/interpreter.py
@@ -1,7 +1,9 @@
import code
import traceback
import sys
-from pygments.token import *
+from pygments.token import Generic, Token, Keyword, Name, Comment, String
+from pygments.token import Error, Literal, Number, Operator, Punctuation
+from pygments.token import Whitespace
from pygments.formatter import Formatter
from bpython.curtsiesfrontend.parse import parse
from codeop import CommandCompiler
From 9fa5f8dbfc701e4f070f699ef990f94637b6a254 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:25:20 +0100
Subject: [PATCH 0135/1650] Use with
Signed-off-by: Sebastian Ramacher
---
bpython/repl.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 8c016434d..427ef363d 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -761,9 +761,8 @@ def write2file(self):
s = self.formatforfile(self.getstdout())
try:
- f = open(fn, mode)
- f.write(s)
- f.close()
+ with open(fn, mode) as f:
+ f.write(s)
except IOError:
self.interact.notify("Disk write error for file '%s'." % (fn, ))
else:
From 6d7fa191396bca1080f6759836f8471a089596cf Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:32:05 +0100
Subject: [PATCH 0136/1650] Verify completion mode
Signed-off-by: Sebastian Ramacher
---
bpython/autocomplete.py | 2 ++
bpython/config.py | 7 +++++++
2 files changed, 9 insertions(+)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index 914eef150..b3e6a95af 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -48,6 +48,8 @@
SUBSTRING = 'substring'
FUZZY = 'fuzzy'
+ALL_MODES = (SIMPLE, SUBSTRING, FUZZY)
+
MAGIC_METHODS = ["__%s__" % s for s in [
"init", "repr", "str", "lt", "le", "eq", "ne", "gt", "ge", "cmp", "hash",
"nonzero", "unicode", "getattr", "setattr", "get", "set","call", "len",
diff --git a/bpython/config.py b/bpython/config.py
index 274cd1f60..0b0abcb16 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -6,6 +6,8 @@
from bpython.keys import cli_key_dispatch as key_dispatch
from bpython.autocomplete import SIMPLE as default_completion
+import bpython.autocomplete
+
class Struct(object):
"""Simple class for instantiating objects we can add arbitrary attributes
to and use for various arbitrary things."""
@@ -229,8 +231,13 @@ def get_key_no_doublebind(attr, already_used={}):
for key in (struct.pastebin_key, struct.save_key):
key_dispatch[key]
+ # expand path of history file
struct.hist_file = os.path.expanduser(struct.hist_file)
+ # verify completion mode
+ if struct.autocomplete_mode not in bpython.autocomplete.ALL_MODES:
+ struct.autocomplete_mode = default_completion
+
def load_theme(struct, path, colors, default_colors):
theme = ConfigParser()
with open(path, 'r') as f:
From e837c925495558d4d91ce885491cbc728ed475ff Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 08:32:18 +0100
Subject: [PATCH 0137/1650] autocomplete_mode is always set
Signed-off-by: Sebastian Ramacher
---
bpython/repl.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/repl.py b/bpython/repl.py
index 427ef363d..d8e5d9bba 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -649,7 +649,7 @@ def complete(self, tab=False):
self.interp.locals,
self.argspec,
'\n'.join(self.buffer + [self.current_line]),
- self.config.autocomplete_mode if hasattr(self.config, 'autocomplete_mode') else autocomplete.SIMPLE,
+ self.config.autocomplete_mode,
self.config.complete_magic_methods)
#TODO implement completer.shown_before_tab == False (filenames shouldn't fill screen)
From bfa13270656dc328d185a856b85a7314b4b12f37 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 09:10:13 +0100
Subject: [PATCH 0138/1650] Compile regular expressions only once
Signed-off-by: Sebastian Ramacher
---
bpython/line.py | 52 ++++++++++++++++++++++++++++++++++---------------
1 file changed, 36 insertions(+), 16 deletions(-)
diff --git a/bpython/line.py b/bpython/line.py
index ee7114670..c3d53b3c6 100644
--- a/bpython/line.py
+++ b/bpython/line.py
@@ -5,10 +5,30 @@
import re
+current_word_re = re.compile(r'[\w_][\w0-9._]*[(]?')
+current_dict_key_re = re.compile(r'''[\w_][\w0-9._]*\[([\w0-9._(), '"]*)''')
+current_dict_re = re.compile(r'''([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)''')
+current_string_re = re.compile(
+ '''(?P(?:""")|"|(?:''\')|')(?:((?P.+?)(?P=open))|(?P.+))''')
+current_object_re = re.compile(r'([\w_][\w0-9_]*)[.]')
+current_object_attribute_re = re.compile(r'([\w_][\w0-9_]*)[.]?')
+current_from_import_from_re = re.compile(r'from ([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*')
+current_from_import_import_re_1 = re.compile(r'from\s([\w0-9_.]*)\s+import')
+current_from_import_import_re_2 = re.compile(r'([\w0-9_]+)')
+current_from_import_import_re_3 = re.compile(r'[,][ ]([\w0-9_]*)')
+current_import_re_1 = re.compile(r'import')
+current_import_re_2 = re.compile(r'([\w0-9_.]+)')
+current_import_re_3 = re.compile(r'[,][ ]([\w0-9_.]*)')
+current_method_definition_name_re = re.compile("def\s+([a-zA-Z_][\w]*)")
+current_single_word_re = re.compile(r"(?= cursor_offset:
return (m.start(1), m.end(1), m.group(1))
@@ -31,7 +51,7 @@ def current_dict_key(cursor_offset, line):
def current_dict(cursor_offset, line):
"""If in dictionary completion, return the dict that should be used"""
- matches = list(re.finditer(r'''([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)''', line))
+ matches = list(current_dict_re.finditer(line))
for m in matches:
if m.start(2) <= cursor_offset and m.end(2) >= cursor_offset:
return (m.start(1), m.end(1), m.group(1))
@@ -42,7 +62,7 @@ def current_string(cursor_offset, line):
Weaker than bpython.Repl's current_string, because that checks that a string is a string
based on previous lines in the buffer"""
- for m in re.finditer('''(?P(?:""")|"|(?:''\')|')(?:((?P.+?)(?P=open))|(?P.+))''', line):
+ for m in current_string_re.finditer(line):
i = 3 if m.group(3) else 4
if m.start(i) <= cursor_offset and m.end(i) >= cursor_offset:
return m.start(i), m.end(i), m.group(i)
@@ -53,7 +73,7 @@ def current_object(cursor_offset, line):
match = current_word(cursor_offset, line)
if match is None: return None
start, end, word = match
- matches = list(re.finditer(r'([\w_][\w0-9_]*)[.]', word))
+ matches = list(current_object_re.finditer(word))
s = ''
for m in matches:
if m.end(1) + start < cursor_offset:
@@ -69,7 +89,7 @@ def current_object_attribute(cursor_offset, line):
match = current_word(cursor_offset, line)
if match is None: return None
start, end, word = match
- matches = list(re.finditer(r'([\w_][\w0-9_]*)[.]?', word))
+ matches = list(current_object_attribute_re.finditer(word))
for m in matches[1:]:
if m.start(1) + start <= cursor_offset and m.end(1) + start >= cursor_offset:
return m.start(1) + start, m.end(1) + start, m.group(1)
@@ -85,7 +105,7 @@ def current_from_import_from(cursor_offset, line):
tokens = line.split()
if not ('from' in tokens or 'import' in tokens):
return None
- matches = list(re.finditer(r'from ([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*', line))
+ matches = list(current_from_import_from_re.finditer(line))
for m in matches:
if ((m.start(1) < cursor_offset and m.end(1) >= cursor_offset) or
(m.start(2) < cursor_offset and m.end(2) >= cursor_offset)):
@@ -97,13 +117,13 @@ def current_from_import_import(cursor_offset, line):
returns None if cursor not in or just after one of these words
"""
- baseline = re.search(r'from\s([\w0-9_.]*)\s+import', line)
+ baseline = current_from_import_import_re_1.search(line)
if baseline is None:
return None
- match1 = re.search(r'([\w0-9_]+)', line[baseline.end():])
+ match1 = current_from_import_import_re_2.search(line[baseline.end():])
if match1 is None:
return None
- matches = list(re.finditer(r'[,][ ]([\w0-9_]*)', line[baseline.end():]))
+ matches = list(current_from_import_import_re_3.finditer(line[baseline.end():]))
for m in [match1] + matches:
start = baseline.end() + m.start(1)
end = baseline.end() + m.end(1)
@@ -113,13 +133,13 @@ def current_from_import_import(cursor_offset, line):
def current_import(cursor_offset, line):
#TODO allow for multiple as's
- baseline = re.search(r'import', line)
+ baseline = current_import_re_1.search(line)
if baseline is None:
return None
- match1 = re.search(r'([\w0-9_.]+)', line[baseline.end():])
+ match1 = current_import_re_2.search(line[baseline.end():])
if match1 is None:
return None
- matches = list(re.finditer(r'[,][ ]([\w0-9_.]*)', line[baseline.end():]))
+ matches = list(current_import_re_3.finditer(line[baseline.end():]))
for m in [match1] + matches:
start = baseline.end() + m.start(1)
end = baseline.end() + m.end(1)
@@ -128,7 +148,7 @@ def current_import(cursor_offset, line):
def current_method_definition_name(cursor_offset, line):
"""The name of a method being defined"""
- matches = re.finditer("def\s+([a-zA-Z_][\w]*)", line)
+ matches = current_method_definition_name_re.finditer(line)
for m in matches:
if (m.start(1) <= cursor_offset and m.end(1) >= cursor_offset):
return m.start(1), m.end(1), m.group(1)
@@ -136,7 +156,7 @@ def current_method_definition_name(cursor_offset, line):
def current_single_word(cursor_offset, line):
"""the un-dotted word just before or under the cursor"""
- matches = re.finditer(r"(?= cursor_offset):
return m.start(1), m.end(1), m.group(1)
@@ -152,7 +172,7 @@ def current_dotted_attribute(cursor_offset, line):
def current_string_literal_attr(cursor_offset, line):
"""The attribute following a string literal"""
- matches = re.finditer("('''" + r'''|"""|'|")((?:(?=([^"'\\]+|\\.|(?!\1)["']))\3)*)\1[.]([a-zA-Z_]?[\w]*)''', line)
+ matches = current_string_literal_attr_re.finditer(line)
for m in matches:
if (m.start(4) <= cursor_offset and m.end(4) >= cursor_offset):
return m.start(4), m.end(4), m.group(4)
From 8afe3a3915cef2c20657e3307502cb50e71f9ab6 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 09:12:40 +0100
Subject: [PATCH 0139/1650] Do not create lists if not neede
---
bpython/line.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/bpython/line.py b/bpython/line.py
index c3d53b3c6..fa460e5a5 100644
--- a/bpython/line.py
+++ b/bpython/line.py
@@ -28,7 +28,7 @@
def current_word(cursor_offset, line):
"""the object.attribute.attribute just before or under the cursor"""
pos = cursor_offset
- matches = list(current_word_re.finditer(line))
+ matches = current_word_re.finditer(line)
start = pos
end = pos
word = None
@@ -43,7 +43,7 @@ def current_word(cursor_offset, line):
def current_dict_key(cursor_offset, line):
"""If in dictionary completion, return the current key"""
- matches = list(current_dict_key_re.finditer(line))
+ matches = current_dict_key_re.finditer(line)
for m in matches:
if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset:
return (m.start(1), m.end(1), m.group(1))
@@ -51,7 +51,7 @@ def current_dict_key(cursor_offset, line):
def current_dict(cursor_offset, line):
"""If in dictionary completion, return the dict that should be used"""
- matches = list(current_dict_re.finditer(line))
+ matches = current_dict_re.finditer(line)
for m in matches:
if m.start(2) <= cursor_offset and m.end(2) >= cursor_offset:
return (m.start(1), m.end(1), m.group(1))
@@ -73,7 +73,7 @@ def current_object(cursor_offset, line):
match = current_word(cursor_offset, line)
if match is None: return None
start, end, word = match
- matches = list(current_object_re.finditer(word))
+ matches = current_object_re.finditer(word)
s = ''
for m in matches:
if m.end(1) + start < cursor_offset:
@@ -105,7 +105,7 @@ def current_from_import_from(cursor_offset, line):
tokens = line.split()
if not ('from' in tokens or 'import' in tokens):
return None
- matches = list(current_from_import_from_re.finditer(line))
+ matches = current_from_import_from_re.finditer(line)
for m in matches:
if ((m.start(1) < cursor_offset and m.end(1) >= cursor_offset) or
(m.start(2) < cursor_offset and m.end(2) >= cursor_offset)):
From d6bb34781a8055e3e76d475b4b6d5bebb40ae262 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 09:13:03 +0100
Subject: [PATCH 0140/1650] Use except Exception as e consistently
Signed-off-by: Sebastian Ramacher
---
bpython/cli.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index f916aba64..c06602950 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -969,7 +969,7 @@ def p_key(self, key):
elif key in key_dispatch[config.show_source_key]:
try:
source = self.get_source_of_current_name()
- except repl.SourceNotFound, e:
+ except repl.SourceNotFound as e:
self.statusbar.message(_(e))
else:
if config.highlight_show_source:
From da2a126fa141348a4767112d563bd31400735930 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 09:26:02 +0100
Subject: [PATCH 0141/1650] Remove remaining lists
Signed-off-by: Sebastian Ramacher
---
bpython/line.py | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/bpython/line.py b/bpython/line.py
index fa460e5a5..21c28a100 100644
--- a/bpython/line.py
+++ b/bpython/line.py
@@ -4,6 +4,8 @@
and return None, or a tuple of the start index, end index, and the word"""
import re
+from itertools import chain
+
current_word_re = re.compile(r'[\w_][\w0-9._]*[(]?')
current_dict_key_re = re.compile(r'''[\w_][\w0-9._]*\[([\w0-9._(), '"]*)''')
@@ -89,8 +91,9 @@ def current_object_attribute(cursor_offset, line):
match = current_word(cursor_offset, line)
if match is None: return None
start, end, word = match
- matches = list(current_object_attribute_re.finditer(word))
- for m in matches[1:]:
+ matches = current_object_attribute_re.finditer(word)
+ matches.next()
+ for m in matches:
if m.start(1) + start <= cursor_offset and m.end(1) + start >= cursor_offset:
return m.start(1) + start, m.end(1) + start, m.group(1)
return None
@@ -123,8 +126,8 @@ def current_from_import_import(cursor_offset, line):
match1 = current_from_import_import_re_2.search(line[baseline.end():])
if match1 is None:
return None
- matches = list(current_from_import_import_re_3.finditer(line[baseline.end():]))
- for m in [match1] + matches:
+ matches = current_from_import_import_re_3.finditer(line[baseline.end():])
+ for m in chain((match1, ), matches):
start = baseline.end() + m.start(1)
end = baseline.end() + m.end(1)
if start < cursor_offset and end >= cursor_offset:
@@ -139,8 +142,8 @@ def current_import(cursor_offset, line):
match1 = current_import_re_2.search(line[baseline.end():])
if match1 is None:
return None
- matches = list(current_import_re_3.finditer(line[baseline.end():]))
- for m in [match1] + matches:
+ matches = current_import_re_3.finditer(line[baseline.end():])
+ for m in chain((match1, ), matches):
start = baseline.end() + m.start(1)
end = baseline.end() + m.end(1)
if start < cursor_offset and end >= cursor_offset:
From d21a9907afd59e9d5ce31c3afb2d4178126c49d0 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 09:26:20 +0100
Subject: [PATCH 0142/1650] Kepp regular expressions close to their use
Signed-off-by: Sebastian Ramacher
---
bpython/line.py | 59 ++++++++++++++++++++++++++++++++++---------------
1 file changed, 41 insertions(+), 18 deletions(-)
diff --git a/bpython/line.py b/bpython/line.py
index 21c28a100..883c6fc96 100644
--- a/bpython/line.py
+++ b/bpython/line.py
@@ -8,24 +8,6 @@
current_word_re = re.compile(r'[\w_][\w0-9._]*[(]?')
-current_dict_key_re = re.compile(r'''[\w_][\w0-9._]*\[([\w0-9._(), '"]*)''')
-current_dict_re = re.compile(r'''([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)''')
-current_string_re = re.compile(
- '''(?P(?:""")|"|(?:''\')|')(?:((?P.+?)(?P=open))|(?P.+))''')
-current_object_re = re.compile(r'([\w_][\w0-9_]*)[.]')
-current_object_attribute_re = re.compile(r'([\w_][\w0-9_]*)[.]?')
-current_from_import_from_re = re.compile(r'from ([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*')
-current_from_import_import_re_1 = re.compile(r'from\s([\w0-9_.]*)\s+import')
-current_from_import_import_re_2 = re.compile(r'([\w0-9_]+)')
-current_from_import_import_re_3 = re.compile(r'[,][ ]([\w0-9_]*)')
-current_import_re_1 = re.compile(r'import')
-current_import_re_2 = re.compile(r'([\w0-9_.]+)')
-current_import_re_3 = re.compile(r'[,][ ]([\w0-9_.]*)')
-current_method_definition_name_re = re.compile("def\s+([a-zA-Z_][\w]*)")
-current_single_word_re = re.compile(r"(?(?:""")|"|(?:''\')|')(?:((?P.+?)(?P=open))|(?P.+))''')
+
def current_string(cursor_offset, line):
"""If inside a string of nonzero length, return the string (excluding quotes)
@@ -70,6 +62,9 @@ def current_string(cursor_offset, line):
return m.start(i), m.end(i), m.group(i)
return None
+
+current_object_re = re.compile(r'([\w_][\w0-9_]*)[.]')
+
def current_object(cursor_offset, line):
"""If in attribute completion, the object on which attribute should be looked up"""
match = current_word(cursor_offset, line)
@@ -86,6 +81,9 @@ def current_object(cursor_offset, line):
return None
return start, start+len(s), s
+
+current_object_attribute_re = re.compile(r'([\w_][\w0-9_]*)[.]?')
+
def current_object_attribute(cursor_offset, line):
"""If in attribute completion, the attribute being completed"""
match = current_word(cursor_offset, line)
@@ -98,6 +96,9 @@ def current_object_attribute(cursor_offset, line):
return m.start(1) + start, m.end(1) + start, m.group(1)
return None
+
+current_from_import_from_re = re.compile(r'from ([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*')
+
def current_from_import_from(cursor_offset, line):
"""If in from import completion, the word after from
@@ -115,6 +116,11 @@ def current_from_import_from(cursor_offset, line):
return m.start(1), m.end(1), m.group(1)
return None
+
+current_from_import_import_re_1 = re.compile(r'from\s([\w0-9_.]*)\s+import')
+current_from_import_import_re_2 = re.compile(r'([\w0-9_]+)')
+current_from_import_import_re_3 = re.compile(r'[,][ ]([\w0-9_]*)')
+
def current_from_import_import(cursor_offset, line):
"""If in from import completion, the word after import being completed
@@ -134,6 +140,11 @@ def current_from_import_import(cursor_offset, line):
return start, end, m.group(1)
return None
+
+current_import_re_1 = re.compile(r'import')
+current_import_re_2 = re.compile(r'([\w0-9_.]+)')
+current_import_re_3 = re.compile(r'[,][ ]([\w0-9_.]*)')
+
def current_import(cursor_offset, line):
#TODO allow for multiple as's
baseline = current_import_re_1.search(line)
@@ -149,6 +160,9 @@ def current_import(cursor_offset, line):
if start < cursor_offset and end >= cursor_offset:
return start, end, m.group(1)
+
+current_method_definition_name_re = re.compile("def\s+([a-zA-Z_][\w]*)")
+
def current_method_definition_name(cursor_offset, line):
"""The name of a method being defined"""
matches = current_method_definition_name_re.finditer(line)
@@ -157,6 +171,9 @@ def current_method_definition_name(cursor_offset, line):
return m.start(1), m.end(1), m.group(1)
return None
+
+current_single_word_re = re.compile(r"(?
Date: Fri, 9 Jan 2015 09:32:19 +0100
Subject: [PATCH 0143/1650] Fix curtsies version constraints
Signed-off-by: Sebastian Ramacher
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 874278737..ea7912977 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,7 +11,7 @@ install:
# core dependencies
- pip install pygments requests
# curtsies specific dependencies
- - pip install 'curtsies<0.2.0' greenlet
+ - pip install 'curtsies >=0.1.15,<0.2.0' greenlet
# translation specific dependencies
- pip install babel
# documentation specific dependencies
From e4a5f66dd389771649f0451d22f1ad9f5f30538e Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 10:42:12 +0100
Subject: [PATCH 0144/1650] Remove pre 2.6 code
Signed-off-by: Sebastian Ramacher
---
bpython/autocomplete.py | 13 ++-----------
1 file changed, 2 insertions(+), 11 deletions(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index b3e6a95af..7bcb28e85 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -24,6 +24,7 @@
from __future__ import with_statement
import __builtin__
import __main__
+import abc
import rlcompleter
import line as lineparts
import re
@@ -33,16 +34,6 @@
from bpython import importcompletion
from bpython._py3compat import py3
-# Needed for special handling of __abstractmethods__
-# abc only exists since 2.6, so check both that it exists and that it's
-# the one we're expecting
-try:
- import abc
- abc.ABCMeta
- has_abc = True
-except (ImportError, AttributeError):
- has_abc = False
-
# Autocomplete modes
SIMPLE = 'simple'
SUBSTRING = 'substring'
@@ -338,7 +329,7 @@ def attr_lookup(obj, expr, attr, autocomplete_mode):
if hasattr(obj, '__class__'):
words.append('__class__')
words = words + rlcompleter.get_class_members(obj.__class__)
- if has_abc and not isinstance(obj.__class__, abc.ABCMeta):
+ if not isinstance(obj.__class__, abc.ABCMeta):
try:
words.remove('__abstractmethods__')
except ValueError:
From c003bab6f19c77225ad173da8203b367ac444f74 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 10:48:47 +0100
Subject: [PATCH 0145/1650] Replace mercurial with git
Signed-off-by: Sebastian Ramacher
---
bpython/__init__.py | 2 +-
doc/sphinx/source/conf.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/bpython/__init__.py b/bpython/__init__.py
index b93f04297..1cce7cc6f 100644
--- a/bpython/__init__.py
+++ b/bpython/__init__.py
@@ -22,7 +22,7 @@
import os.path
-__version__ = 'mercurial'
+__version__ = 'git'
package_dir = os.path.abspath(os.path.dirname(__file__))
diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py
index 805ef7e8f..348a07331 100644
--- a/doc/sphinx/source/conf.py
+++ b/doc/sphinx/source/conf.py
@@ -45,9 +45,9 @@
# built documents.
#
# The short X.Y version.
-version = 'mercurial'
+version = 'git'
# The full version, including alpha/beta/rc tags.
-release = 'mercurial'
+release = 'git'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
From eb499b87bdbaf02f9f4dee5692cd478b6b4ce4e3 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 10:54:33 +0100
Subject: [PATCH 0146/1650] Add links to documentation (fixes #383)
Signed-off-by: Sebastian Ramacher
---
README.rst | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/README.rst b/README.rst
index 951f55c1b..b53963d5b 100644
--- a/README.rst
+++ b/README.rst
@@ -159,4 +159,11 @@ To correct this I have provided my windows.theme file.
This curses implementation has 16 colors (dark and light versions of the
colours)
+See also
+========
+
+Documentation
+ http://docs.bpython-interpreter.org/
+Developer documentation
+ http://docs.bpython-interpreter.org/contributing.html
From ad584cb77634932d8632f2e7ea54ce0e06b510f2 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 11:05:30 +0100
Subject: [PATCH 0147/1650] Store compiled regex
Signed-off-by: Sebastian Ramacher
---
bpython/autocomplete.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py
index 7bcb28e85..8243ec2ab 100644
--- a/bpython/autocomplete.py
+++ b/bpython/autocomplete.py
@@ -297,6 +297,8 @@ def safe_eval(expr, namespace):
raise EvaluationError
+attr_matches_re = re.compile(r"(\w+(\.\w+)*)\.(\w*)")
+
def attr_matches(text, namespace, autocomplete_mode):
"""Taken from rlcompleter.py and bent to my will.
"""
@@ -304,7 +306,7 @@ def attr_matches(text, namespace, autocomplete_mode):
# Gna, Py 2.6's rlcompleter searches for __call__ inside the
# instance instead of the type, so we monkeypatch to prevent
# side-effects (__getattr__/__getattribute__)
- m = re.match(r"(\w+(\.\w+)*)\.(\w*)", text)
+ m = attr_matches_re.match(text)
if not m:
return []
From b07e149d29205cbbe04739cb89a17930c6537ef7 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 13:49:15 +0100
Subject: [PATCH 0148/1650] Move tests to bpython.tests
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/coderunner.py | 25 ----------------
bpython/test/test_curtsies_coderunner.py | 38 ++++++++++++++++++++++++
2 files changed, 38 insertions(+), 25 deletions(-)
create mode 100644 bpython/test/test_curtsies_coderunner.py
diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py
index e10897ee2..99314d08e 100644
--- a/bpython/curtsiesfrontend/coderunner.py
+++ b/bpython/curtsiesfrontend/coderunner.py
@@ -186,28 +186,3 @@ def flush(self):
def isatty(self):
return True
-def test_simple():
- orig_stdout = sys.stdout
- orig_stderr = sys.stderr
- c = CodeRunner(request_refresh=lambda: orig_stdout.flush() or orig_stderr.flush())
- stdout = FakeOutput(c, orig_stdout.write)
- sys.stdout = stdout
- c.load_code('1 + 1')
- c.run_code()
- c.run_code()
- c.run_code()
-
-def test_exception():
- orig_stdout = sys.stdout
- orig_stderr = sys.stderr
- c = CodeRunner(request_refresh=lambda: orig_stdout.flush() or orig_stderr.flush())
- def ctrlc():
- raise KeyboardInterrupt()
- stdout = FakeOutput(c, lambda x: ctrlc())
- sys.stdout = stdout
- c.load_code('1 + 1')
- c.run_code()
-
-if __name__ == '__main__':
- test_simple()
-
diff --git a/bpython/test/test_curtsies_coderunner.py b/bpython/test/test_curtsies_coderunner.py
new file mode 100644
index 000000000..80021503d
--- /dev/null
+++ b/bpython/test/test_curtsies_coderunner.py
@@ -0,0 +1,38 @@
+import unittest
+import sys
+
+from bpython.curtsiesfrontend.coderunner import CodeRunner, FakeOutput
+
+class TestCodeRunner(unittest.TestCase):
+
+ def setUp(self):
+ self.orig_stdout = sys.stdout
+ self.orig_stderr = sys.stderr
+
+ def tearDown(self):
+ sys.stdout = self.orig_stdout
+ sys.stderr = self.orig_stderr
+
+ def test_simple(self):
+ c = CodeRunner(request_refresh=lambda: self.orig_stdout.flush() or self.orig_stderr.flush())
+ stdout = FakeOutput(c, lambda *args, **kwargs: None)
+ stderr = FakeOutput(c, lambda *args, **kwargs: None)
+ sys.stdout = stdout
+ sys.stdout = stderr
+ c.load_code('1 + 1')
+ c.run_code()
+ c.run_code()
+ c.run_code()
+
+ def test_exception(self):
+ orig_stdout = sys.stdout
+ orig_stderr = sys.stderr
+ c = CodeRunner(request_refresh=lambda: self.orig_stdout.flush() or self.orig_stderr.flush())
+ def ctrlc():
+ raise KeyboardInterrupt()
+ stdout = FakeOutput(c, lambda x: ctrlc())
+ stderr = FakeOutput(c, lambda *args, **kwargs: None)
+ sys.stdout = stdout
+ sys.stderr = stderr
+ c.load_code('1 + 1')
+ c.run_code()
From d28b3ab15dd61c9f1f9dc5af9ed7cf7105572a40 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 13:49:29 +0100
Subject: [PATCH 0149/1650] Add newlines
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/repl.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index ee1b55227..b53d39367 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -1214,6 +1214,7 @@ def __repr__(self):
def _get_current_line(self):
return self._current_line
+
def _set_current_line(self, line, update_completion=True, reset_rl_history=True, clear_special_mode=True):
if self._current_line == line:
return
@@ -1227,8 +1228,10 @@ def _set_current_line(self, line, update_completion=True, reset_rl_history=True,
self.unhighlight_paren()
current_line = property(_get_current_line, _set_current_line, None,
"The current line")
+
def _get_cursor_offset(self):
return self._cursor_offset
+
def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=False, clear_special_mode=True):
if self._cursor_offset == offset:
return
@@ -1241,8 +1244,10 @@ def _set_cursor_offset(self, offset, update_completion=True, reset_rl_history=Fa
self._cursor_offset = offset
self.update_completion()
self.unhighlight_paren()
+
cursor_offset = property(_get_cursor_offset, _set_cursor_offset, None,
"The current cursor offset from the front of the line")
+
def echo(self, msg, redraw=True):
"""
Notification that redrawing the current line is necessary (we don't
@@ -1252,10 +1257,12 @@ def echo(self, msg, redraw=True):
It's not supposed to update the screen if it's reevaluating the code (as it
does with undo)."""
logger.debug("echo called with %r" % msg)
+
@property
def cpos(self):
"many WATs were had - it's the pos from the end of the line back"""
return len(self.current_line) - self.cursor_offset
+
def reprint_line(self, lineno, tokens):
logger.debug("calling reprint line with %r %r", lineno, tokens)
if self.config.syntax:
From 347c7c88c475375b839a6dda274589a02eca1c56 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 15:24:51 +0100
Subject: [PATCH 0150/1650] Switch default binary to curtsies
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/man-bpython.rst | 4 ++--
setup.py | 26 ++++++++++++++------------
2 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/doc/sphinx/source/man-bpython.rst b/doc/sphinx/source/man-bpython.rst
index 2242633a7..6a0fa8398 100644
--- a/doc/sphinx/source/man-bpython.rst
+++ b/doc/sphinx/source/man-bpython.rst
@@ -8,9 +8,9 @@ Synopsis
**bpython** [*options*] [*file* [*args*]]
-**bpython-urwid** [*options*] [*file* [*args*]]
+**bpython-curses** [*options*] [*file* [*args*]]
-**bpython-curtsies** [*options*] [*file* [*args*]]
+**bpython-urwid** [*options*] [*file* [*args*]]
Description
diff --git a/setup.py b/setup.py
index 1818471ae..4f2898f59 100755
--- a/setup.py
+++ b/setup.py
@@ -125,30 +125,32 @@ def initialize_options(self):
install_requires = [
'pygments',
- 'requests'
+ 'requests',
+ 'curtsies >=0.1.15, <0.2.0',
+ 'greenlet'
]
extras_require = {
- 'urwid' : ['urwid']
+ 'urwid' : ['urwid'],
+ 'watch' : ['watchdog']
}
-packages = ['bpython', 'bpython.test', 'bpython.translations', 'bpdb']
+packages = [
+ 'bpython',
+ 'bpython.curtsiesfrontend',
+ 'bpython.test',
+ 'bpython.translations',
+ 'bpdb'
+]
entry_points = {
'console_scripts': [
- 'bpython = bpython.cli:main',
+ 'bpython = bpython.curtsies:main',
+ 'bpython-curses = bpython.cli:main',
'bpython-urwid = bpython.urwid:main [urwid]'
]
}
-if sys.version_info[:2] >= (2, 6):
- # curtsies only supports 2.6 and onwards
- extras_require['curtsies'] = ['curtsies >=0.1.15, <0.2.0', 'greenlet']
- extras_require['watch'] = ['watchdog']
- packages.append("bpython.curtsiesfrontend")
- entry_points['console_scripts'].append(
- 'bpython-curtsies = bpython.curtsies:main [curtsies]')
-
if sys.version_info[0] == 2 and sys.platform == "darwin":
# need PyOpenSSL for SNI support (only 2.X and on Darwin)
# list of packages taken from
From 25838bfad2e68c39cb36ae56d05b1ea5991472e0 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 16:33:02 +0100
Subject: [PATCH 0151/1650] Automatic versioning (fixes #333)
Signed-off-by: Sebastian Ramacher
---
.gitignore | 1 +
bpython/__init__.py | 4 +++-
setup.py | 30 +++++++++++++++++++++++++++---
3 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
index 62cb91fb7..f5a04a965 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ env
.DS_Store
.idea/
doc/sphinx/build/*
+bpython/_version.py
diff --git a/bpython/__init__.py b/bpython/__init__.py
index 1cce7cc6f..19bb7edbb 100644
--- a/bpython/__init__.py
+++ b/bpython/__init__.py
@@ -21,8 +21,10 @@
# THE SOFTWARE.
import os.path
+import bpython._version
-__version__ = 'git'
+
+__version__ = bpython._version.__version__
package_dir = os.path.abspath(os.path.dirname(__file__))
diff --git a/setup.py b/setup.py
index 4f2898f59..b20fb4c3d 100755
--- a/setup.py
+++ b/setup.py
@@ -5,12 +5,13 @@
import os
import platform
import sys
+import subprocess
from distutils.command.build import build
from setuptools import setup
from setuptools.command.install import install as _install
-from bpython import __version__, package_dir
+from bpython import package_dir
try:
from babel.messages.frontend import compile_catalog as _compile_catalog
@@ -33,6 +34,25 @@
except ImportError:
using_sphinx = False
+
+# version handling
+version_file = 'bpython/_version.py'
+
+try:
+ # get version from git describe
+ version = subprocess.check_output(['git', 'describe', '--tags']).rstrip()
+except OSError:
+ try:
+ # get version from existing version file
+ with open(version_file) as vf:
+ version = vf.read().strip().split('=')[-1].replace('\'', '')
+ except IOError:
+ version = 'unknown'
+
+with open(version_file, 'w') as vf:
+ vf.write('# Auto-generated file, do not edit!\n')
+ vf.write('__version__=\'%s\'\n' % (version, ))
+
class install(_install):
"""Force install to run build target."""
@@ -40,7 +60,11 @@ def run(self):
self.run_command('build')
_install.run(self)
-cmdclass = dict(build=build, install=install)
+cmdclass = {
+ 'build': build,
+ 'install': install
+}
+
translations_dir = os.path.join(package_dir, 'translations')
# localization options
@@ -172,7 +196,7 @@ def initialize_options(self):
setup(
name="bpython",
- version = __version__,
+ version = version,
author = "Bob Farrell, Andreas Stuehrk et al.",
author_email = "robertanthonyfarrell@gmail.com",
description = "Fancy Interface to the Python Interpreter",
From 843de503e4cd55c2fab7593d121159e7ea08ec40 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 16:41:58 +0100
Subject: [PATCH 0152/1650] Use nicer version numbers
Signed-off-by: Sebastian Ramacher
---
setup.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/setup.py b/setup.py
index b20fb4c3d..e627040e5 100755
--- a/setup.py
+++ b/setup.py
@@ -41,6 +41,14 @@
try:
# get version from git describe
version = subprocess.check_output(['git', 'describe', '--tags']).rstrip()
+ version_split = version.split('-')
+ if len(version_split) == 4:
+ # format: version-release-commits-hash
+ version = '-'.join((version_split[0], version_split[2]))
+ elif len(version_split) == 2:
+ # format: version-release
+ version = version_split[0]
+
except OSError:
try:
# get version from existing version file
From 717206cfe21ee89fa1d2afdf3f756621db2d617f Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 16:44:16 +0100
Subject: [PATCH 0153/1650] There is no tex documentaiton
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/conf.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py
index 348a07331..3a76bda0c 100644
--- a/doc/sphinx/source/conf.py
+++ b/doc/sphinx/source/conf.py
@@ -171,10 +171,10 @@
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
- ('index', 'bpython.tex', u'bpython Documentation',
- u'Robert Farrell', 'manual'),
-]
+#latex_documents = [
+# ('index', 'bpython.tex', u'bpython Documentation',
+# u'Robert Farrell', 'manual'),
+#]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
From 1094abe6f4c3dcaafb376f1a5d964189af7bc589 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 16:47:25 +0100
Subject: [PATCH 0154/1650] Use auto-generated version number for sphinx
documentation
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/conf.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py
index 3a76bda0c..85fb27ee9 100644
--- a/doc/sphinx/source/conf.py
+++ b/doc/sphinx/source/conf.py
@@ -11,7 +11,7 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -45,9 +45,15 @@
# built documents.
#
# The short X.Y version.
-version = 'git'
+
+version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ '../../../bpython/_version.py')
+
+with open(version_file) as vf:
+ version = vf.read().strip().split('=')[-1].replace('\'', '')
+
# The full version, including alpha/beta/rc tags.
-release = 'git'
+release = version
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
From 74ce9642236dd4be23a533ec123cef7d71eaca3b Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 16:55:16 +0100
Subject: [PATCH 0155/1650] Do not fail if bpython._version does not exist
Signed-off-by: Sebastian Ramacher
---
bpython/__init__.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/bpython/__init__.py b/bpython/__init__.py
index 19bb7edbb..505ef61e2 100644
--- a/bpython/__init__.py
+++ b/bpython/__init__.py
@@ -21,10 +21,13 @@
# THE SOFTWARE.
import os.path
-import bpython._version
+try:
+ from bpython._version import __version__ as version
+except ImportError:
+ version = 'unkown'
-__version__ = bpython._version.__version__
+__version__ = version
package_dir = os.path.abspath(os.path.dirname(__file__))
From 8f628bbf6811a8b5f8cb76dcf93c94120d92338f Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 17:11:56 +0100
Subject: [PATCH 0156/1650] Catch correct exception
Signed-off-by: Sebastian Ramacher
---
setup.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index e627040e5..c02fe1874 100755
--- a/setup.py
+++ b/setup.py
@@ -48,8 +48,7 @@
elif len(version_split) == 2:
# format: version-release
version = version_split[0]
-
-except OSError:
+except subprocess.CalledProcessError:
try:
# get version from existing version file
with open(version_file) as vf:
From f66393c88427179d459674aa63d4e8701651ea6d Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 17:16:10 +0100
Subject: [PATCH 0157/1650] Import package_dir as late as possible
Signed-off-by: Sebastian Ramacher
---
setup.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index c02fe1874..82024bdc0 100755
--- a/setup.py
+++ b/setup.py
@@ -11,8 +11,6 @@
from setuptools import setup
from setuptools.command.install import install as _install
-from bpython import package_dir
-
try:
from babel.messages.frontend import compile_catalog as _compile_catalog
from babel.messages.frontend import extract_messages as _extract_messages
@@ -72,6 +70,7 @@ def run(self):
'install': install
}
+from bpython import package_dir
translations_dir = os.path.join(package_dir, 'translations')
# localization options
From 240c8dd2ffa0e9b2a6450c89aa1e23b5bf0ab5f5 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 17:49:13 +0100
Subject: [PATCH 0158/1650] Fix 2.6 incompatibility
Signed-off-by: Sebastian Ramacher
---
setup.py | 28 ++++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/setup.py b/setup.py
index 82024bdc0..930a6ab92 100755
--- a/setup.py
+++ b/setup.py
@@ -35,24 +35,32 @@
# version handling
version_file = 'bpython/_version.py'
+version = 'unkown'
try:
# get version from git describe
- version = subprocess.check_output(['git', 'describe', '--tags']).rstrip()
- version_split = version.split('-')
- if len(version_split) == 4:
- # format: version-release-commits-hash
- version = '-'.join((version_split[0], version_split[2]))
- elif len(version_split) == 2:
- # format: version-release
- version = version_split[0]
-except subprocess.CalledProcessError:
+ proc = subprocess.Popen(['git', 'describe', '--tags'],
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout = proc.communicate()[0].rstrip()
+
+ if proc.returncode == 0:
+ version_split = stdout.split('-')
+ if len(version_split) == 4:
+ # format: version-release-commits-hash
+ version = '-'.join((version_split[0], version_split[2]))
+ elif len(version_split) == 2:
+ # format: version-release
+ version = version_split[0]
+except OSError:
+ pass
+
+if version == 'unknown':
try:
# get version from existing version file
with open(version_file) as vf:
version = vf.read().strip().split('=')[-1].replace('\'', '')
except IOError:
- version = 'unknown'
+ pass
with open(version_file, 'w') as vf:
vf.write('# Auto-generated file, do not edit!\n')
From e34e756af1b95464f1c0efdcffe00f1cdbb985d5 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 18:02:50 +0100
Subject: [PATCH 0159/1650] Update dependencies
Signed-off-by: Sebastian Ramacher
---
README.rst | 13 +++----------
1 file changed, 3 insertions(+), 10 deletions(-)
diff --git a/README.rst b/README.rst
index b53963d5b..7fc8fc258 100644
--- a/README.rst
+++ b/README.rst
@@ -12,17 +12,10 @@ Dependencies
* Sphinx != 1.1.2 (for the documentation only) (apt-get install python-sphinx)
* mock (for the testsuite only)
* babel (optional, for internationalization)
-
-bpython-curtsies
-----------------
-``bpython-curtsies`` requires the following additional packages:
-
-* curtsies >= 0.1.0
+* requests
+* curtsies >= 0.1.15,<0.2.0
* greenlet
-
-and optionally (for monitoring imported modules for changes)
-
-* watchdog
+* watchdog (optional, (for monitoring imported modules for changes)
bpython-urwid
-------------
From 958fce83314dfdae6507a1d3f0a9337b3f58fbad Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 18:06:57 +0100
Subject: [PATCH 0160/1650] Update Python version numbers
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/releases.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/sphinx/source/releases.rst b/doc/sphinx/source/releases.rst
index 06d20a624..8443628d3 100644
--- a/doc/sphinx/source/releases.rst
+++ b/doc/sphinx/source/releases.rst
@@ -34,7 +34,7 @@ A checklist to perform some manual tests before a release:
Check that all of the following work before a release:
-* Runs under Python 2.5, 2.6 and 3.1 (after 2to3).
+* Runs under Python 2.6, 2.7, 3.3, and 3.4 (after 2to3).
* Save
* Rewind
* Pastebin
From 0c0841476d0c2590aa65f51991d04ddec190decf Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 9 Jan 2015 13:28:19 -0500
Subject: [PATCH 0161/1650] update contrib docs
use pip install -e . instead of python setup.py develop
install curtsies with above command instead of manually
(this should prevent curtsies version problems)
change suggested command from bpython-curtsies to bpython
---
doc/sphinx/source/contributing.rst | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index a63c685f7..3ae5f6835 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -18,12 +18,13 @@ Getting your development environment set up
bpython supports Python 2.6, 2.7, 3.3 and 3.4. The code is written in Python
2 and transformed for Python 3 with 2to3. This means that it's easier
-to work on bpython with Python 2 because it's you can use `python setup.py develop`
+to work on bpython with Python 2 because you can use ``python setup.py develop``
+or ``pip install -e .``
(`development mode
`_ installs
by linking to the bpython source instead of copying it over)
in a more straightforward way to have your changes immediately reflected by
-your bpython installation and the `bpython`, `bpython-curtsies`, and `bpython-urwid`
+your bpython installation and the `bpython`, `bpython-curses`, and `bpython-urwid`
commands.
Using a virtual environment is probably a good idea. Create a virtual environment with
@@ -31,7 +32,7 @@ Using a virtual environment is probably a good idea. Create a virtual environmen
.. code-block:: bash
$ virtualenv bpython-dev # determines Python version used
- $ source bpython-dev/bin/activate # necssary every time you work on bpython
+ $ source bpython-dev/bin/activate # necessary every time you work on bpython
Fork bpython in the GitHub web interface, then clone the repo:
@@ -40,17 +41,16 @@ Fork bpython in the GitHub web interface, then clone the repo:
$ git clone git@github.com:YOUR_GITHUB_USERNAME/bpython.git
$ # or "git clone https://github.com/YOUR_GITHUB_USERNAME/bpython.git"
-Next install the dependencies and install your development copy of bpython:
+Next install the install your development copy of bpython and its dependencies:
.. code-block:: bash
- $ pip install pygments requests # install required dependencies
- $ pip install curtsies greenlet watchdog urwid # install optional dependencies
- $ pip install sphinx mock nosetests # development dependencies
$ cd bpython
- $ python setup.py develop
+ $ pip install -e . # install bpython and required dependencies
+ $ pip install watchdog urwid # install optional dependencies
+ $ pip install sphinx mock nose # development dependencies
- $ bpython-curtsies # this runs your modified copy of bpython!
+ $ bpython # this runs your modified copy of bpython!
As a first dev task, I recommend getting `bpython` to print your name every time you hit a specific key.
@@ -60,8 +60,8 @@ To run tests from the bpython directory:
$ nosetests
-To build the docs:
-------------------
+Building the documentation
+--------------------------
The documentation is included in the bpython repository. After
checking out the bpython repository and installing `sphinx` as described in
@@ -75,7 +75,7 @@ repository to build the documentation:
Afterwards you can point your browser to `doc/sphinx/build/html/index.html`.
Don't forget to recreate the HTML after you make changes.
-To hack on the site or theme
+Hacking on the site or theme
----------------------------
The site (and its theme as well) is stored in a separate repository and built using
From 347105eb0cb1dbcbf7c5fb3f0c75ab0dc62647c8 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 19:52:45 +0100
Subject: [PATCH 0162/1650] Update requirements
Signed-off-by: Sebastian Ramacher
---
requirements.txt | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index 95f1fbda3..7fd8f8ae4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
Pygments
-argparse
-curtsies
-distribute
+curtsies >=0.1.15, <0.2.0
greenlet
+setuptools
+requests
From 19cd412090165abf22b965a1e4db324d3261f7c7 Mon Sep 17 00:00:00 2001
From: Thomas Ballinger
Date: Fri, 9 Jan 2015 15:27:36 -0500
Subject: [PATCH 0163/1650] recommend using a package manager
---
doc/sphinx/source/contributing.rst | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index 3ae5f6835..bba3fabba 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -52,6 +52,10 @@ Next install the install your development copy of bpython and its dependencies:
$ bpython # this runs your modified copy of bpython!
+Many requirements are also available from your distribution's package manager.
+Installation of some dependencies requires Python headers and a C compiler.
+These are also available from your package manager.
+
As a first dev task, I recommend getting `bpython` to print your name every time you hit a specific key.
To run tests from the bpython directory:
From e33f6f1af55ab4872161417777bc65b01899c390 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sat, 10 Jan 2015 18:34:37 +0100
Subject: [PATCH 0164/1650] Pack the note about package managers in a note env
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/contributing.rst | 28 ++++++++++++++++++++++++----
1 file changed, 24 insertions(+), 4 deletions(-)
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index bba3fabba..0ca85b684 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -52,11 +52,31 @@ Next install the install your development copy of bpython and its dependencies:
$ bpython # this runs your modified copy of bpython!
-Many requirements are also available from your distribution's package manager.
-Installation of some dependencies requires Python headers and a C compiler.
-These are also available from your package manager.
+.. note::
-As a first dev task, I recommend getting `bpython` to print your name every time you hit a specific key.
+ Many requirements are also available from your distribution's package
+ manager. On Debian/Ubuntu based systems, the following packages can be used:
+
+ .. code-block:: bash
+
+ $ sudp apt-get install python-greenlet python-pygments python-requests
+ $ sudo apt-get install python-watchdog python-urwid
+ $ sudo apt-get install python-sphinx python-mock python-nose
+
+ Rememeber to replace ``python`` with ``python3`` in every package name if
+ you intend to develop with Python 3. You also need to run `virtualenv` with
+ `--system-site-packages` packages, if you want to use the packages provided
+ by your distribution.
+
+ Installation of some dependencies with ``pip`` requires Python headers and a
+ C compiler. These are also available from your package manager.
+
+ .. code-block:: bash
+
+ $ sudo apt-get install gcc python-dev
+
+As a first dev task, I recommend getting `bpython` to print your name every
+time you hit a specific key.
To run tests from the bpython directory:
From a609304b7391cc618a84e5b457f44e0429856079 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sat, 10 Jan 2015 18:36:10 +0100
Subject: [PATCH 0165/1650] Make note about compiler and python-dev seperate
note
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/contributing.rst | 2 ++
1 file changed, 2 insertions(+)
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index 0ca85b684..37d83d7c1 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -68,6 +68,8 @@ Next install the install your development copy of bpython and its dependencies:
`--system-site-packages` packages, if you want to use the packages provided
by your distribution.
+.. note::
+
Installation of some dependencies with ``pip`` requires Python headers and a
C compiler. These are also available from your package manager.
From bf5cf3c704b1e327823aa0d2dcce0085233203b4 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sat, 10 Jan 2015 18:41:01 +0100
Subject: [PATCH 0166/1650] Remove a space
Signed-off-by: Sebastian Ramacher
---
doc/sphinx/source/contributing.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst
index a63c685f7..222414871 100644
--- a/doc/sphinx/source/contributing.rst
+++ b/doc/sphinx/source/contributing.rst
@@ -109,5 +109,5 @@ included configuration file.
After this you can open the `output/index.html` in your favourite browser and see
if your changes had an effect.
-.. _GitHub issue tracker: https://github.com/bpython/bpython/issues
+.. _GitHub issue tracker: https://github.com/bpython/bpython/issues
.. _bite-size: https://github.com/bpython/bpython/labels/bitesize
From aabcd6d3797e59ec198d4f9721e4bc0230363ba5 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 03:50:03 +0100
Subject: [PATCH 0167/1650] Import RefreshReqeustEvent from
bpython.curtsiesfrontend.events (fixes #436)
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/interaction.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py
index 1036471c1..2d017636c 100644
--- a/bpython/curtsiesfrontend/interaction.py
+++ b/bpython/curtsiesfrontend/interaction.py
@@ -3,7 +3,7 @@
import curtsies.events as events
from bpython.repl import Interaction as BpythonInteraction
-
+from bpython.curtsiesfrontend.events import RefreshRequestEvent
from bpython.curtsiesfrontend.manual_readline import edit_keys
class StatusBar(BpythonInteraction):
@@ -67,7 +67,7 @@ def _check_for_expired_message(self):
def process_event(self, e):
"""Returns True if shutting down"""
assert self.in_prompt or self.in_confirm or self.waiting_for_refresh
- if isinstance(e, events.RefreshRequestEvent):
+ if isinstance(e, RefreshRequestEvent):
self.waiting_for_refresh = False
self.request_greenlet.switch()
elif isinstance(e, events.PasteEvent):
From 83b80ae60c26ce41ab80bc5ecdda8f19c719146d Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 04:03:57 +0100
Subject: [PATCH 0168/1650] Hide watchdog message after some time (fixes #437)
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/repl.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index b53d39367..f49fec455 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -738,10 +738,9 @@ def toggle_file_watch(self):
if self.watching_files:
self.watcher.deactivate()
self.watching_files = False
- self.status_bar.pop_permanent_message(msg)
else:
self.watching_files = True
- self.status_bar.push_permanent_message(msg)
+ self.status_bar.message(msg)
self.watcher.activate()
else:
self.status_bar.message('Autoreloading not available because watchdog not installed')
From 5bf22e4f2f2973fe1b42e02d4e10ae10f9fd6ae5 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 05:44:49 +0100
Subject: [PATCH 0169/1650] Implement copy to clipboard (fixes #234)
Based on a patch by Keith Dart
Signed-off-by: Sebastian Ramacher
---
bpython/cli.py | 4 ++
bpython/clipboard.py | 67 +++++++++++++++++++++++++
bpython/config.py | 2 +
bpython/curtsiesfrontend/interaction.py | 1 +
bpython/curtsiesfrontend/repl.py | 2 +
bpython/repl.py | 17 +++++++
6 files changed, 93 insertions(+)
create mode 100644 bpython/clipboard.py
diff --git a/bpython/cli.py b/bpython/cli.py
index c06602950..cad3a907f 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -962,6 +962,10 @@ def p_key(self, key):
self.pastebin()
return ''
+ elif key in key_dispatch[config.copy_clipboard_key]:
+ self.copy2clipboard()
+ return ''
+
elif key in key_dispatch[config.last_output_key]:
page(self.stdout_hist[self.prev_block_finished:-4])
return ''
diff --git a/bpython/clipboard.py b/bpython/clipboard.py
new file mode 100644
index 000000000..25302863d
--- /dev/null
+++ b/bpython/clipboard.py
@@ -0,0 +1,67 @@
+# The MIT License
+#
+# Copyright (c) 2015 the bpython authors.
+#
+# 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.
+
+import subprocess
+import os
+import platform
+from locale import getpreferredencoding
+
+class CopyFailed(Exception):
+ pass
+
+class XClipboard(object):
+ """Manage clipboard with xclip."""
+
+ def copy(self, content):
+ process = subprocess.Popen(['xclip', '-i', '-selection', 'clipboard'],
+ stdin=subprocess.PIPE)
+ process.communicate(content.encode(getpreferredencoding()))
+ if process.returncode != 0:
+ raise CopyFailed()
+
+class OSXClipboard(object):
+ """Manage clipboard with pbcopy."""
+
+ def copy(self, content):
+ process = subprocess.Popen(['pbcopy', 'w'], stdin=subprocess.PIPE)
+ process.communicate(content.encode(getpreferredencoding()))
+ if process.returncode != 0:
+ raise CopyFailed()
+
+def command_exists(command):
+ process = subprocess.Popen(['which', command], stderr=subprocess.STDOUT,
+ stdout=subprocess.PIPE)
+ process.communicate()
+
+ return process.returncode == 0
+
+def get_clipboard():
+ """Get best clipboard handling implemention for current system."""
+
+ if platform.system() == 'Darwin':
+ if command_exists('pbcopy'):
+ return OSXClipboard()
+ if platform.system() in ('Linux', 'FreeBSD', 'OpenBSD') and os.getenv('DISPLAY') is not None:
+ if command_exists('xclip'):
+ return XClipboard()
+
+ return None
diff --git a/bpython/config.py b/bpython/config.py
index 0b0abcb16..5ee3149dc 100644
--- a/bpython/config.py
+++ b/bpython/config.py
@@ -82,6 +82,7 @@ def loadini(struct, configfile):
'edit_current_block': 'C-x',
'help': 'F1',
'last_output': 'F9',
+ 'copy_clipboard': 'F10',
'pastebin': 'F8',
'save': 'C-s',
'show_source': 'F2',
@@ -139,6 +140,7 @@ def get_key_no_doublebind(attr, already_used={}):
struct.flush_output = config.getboolean('general', 'flush_output')
struct.pastebin_key = get_key_no_doublebind('pastebin')
+ struct.copy_clipboard_key = get_key_no_doublebind('copy_clipboard')
struct.save_key = get_key_no_doublebind('save')
struct.search_key = get_key_no_doublebind('search')
struct.show_source_key = get_key_no_doublebind('show_source')
diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py
index 2d017636c..faf2dfaa3 100644
--- a/bpython/curtsiesfrontend/interaction.py
+++ b/bpython/curtsiesfrontend/interaction.py
@@ -143,6 +143,7 @@ def confirm(self, q):
self.prompt = q
self.in_confirm = True
return self.main_greenlet.switch(q)
+
def file_prompt(self, s):
"""Expected to return a file name, given """
self.request_greenlet = greenlet.getcurrent()
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index f49fec455..42b988ec9 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -547,6 +547,8 @@ def process_key_event(self, e):
greenlet.greenlet(self.write2file).switch()
elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin
greenlet.greenlet(self.pastebin).switch()
+ elif e in key_dispatch[self.config.copy_clipboard_key]:
+ greenlet.greenlet(self.copy2clipboard).switch()
elif e in key_dispatch[self.config.external_editor_key]:
self.send_session_to_external_editor()
elif e in key_dispatch[self.config.edit_config_key]:
diff --git a/bpython/repl.py b/bpython/repl.py
index d8e5d9bba..3d579818c 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -48,6 +48,7 @@
from bpython._py3compat import PythonLexer, py3
from bpython.formatter import Parenthesis
from bpython.translations import _
+from bpython.clipboard import get_clipboard, CopyFailed
import bpython.autocomplete as autocomplete
@@ -446,6 +447,7 @@ def __init__(self, interp, config):
# Necessary to fix mercurial.ui.ui expecting sys.stderr to have this
# attribute
self.closed = False
+ self.clipboard = get_clipboard()
pythonhist = os.path.expanduser(self.config.hist_file)
if os.path.exists(pythonhist):
@@ -768,6 +770,21 @@ def write2file(self):
else:
self.interact.notify('Saved to %s.' % (fn, ))
+ def copy2clipboard(self):
+ """Copy current content to clipboard."""
+
+ if self.clipboard is None:
+ self.interact.notify(_('No clipboard available.'))
+ return
+
+ content = self.formatforfile(self.getstdout())
+ try:
+ self.clipboard.copy(content)
+ except CopyFailed:
+ self.interact.notify(_('Could not copy to clipboard.'))
+ else:
+ self.interact.notify(_('Copied content to clipboard.'))
+
def pastebin(self, s=None):
"""Upload to a pastebin and display the URL in the status bar."""
From d4db7edf4abb3f65c2d8a6ab072543e9ccb3b0d0 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 05:53:45 +0100
Subject: [PATCH 0170/1650] Do not show disabled keyboard shortcuts (fixes
#282)
Signed-off-by: Sebastian Ramacher
---
bpython/cli.py | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/bpython/cli.py b/bpython/cli.py
index cad3a907f..3728b35fe 100644
--- a/bpython/cli.py
+++ b/bpython/cli.py
@@ -1692,12 +1692,19 @@ def init_wins(scr, config):
# Thanks to Angus Gibson for pointing out this missing line which was causing
# problems that needed dirty hackery to fix. :)
- statusbar = Statusbar(scr, main_win, background, config,
- _(" <%s> Rewind <%s> Save <%s> Pastebin "
- " <%s> Pager <%s> Show Source ") %
- (config.undo_key, config.save_key, config.pastebin_key,
- config.last_output_key, config.show_source_key),
- get_colpair(config, 'main'))
+ commands = (
+ (_('Rewind'), config.undo_key),
+ (_('Save'), config.save_key),
+ (_('Pastebin'), config.pastebin_key),
+ (_('Pager'), config.last_output_key),
+ (_('Show Source'), config.show_source_key)
+ )
+
+ message = ' '.join('<%s> %s' % (key, command) for command, key in commands
+ if key)
+
+ statusbar = Statusbar(scr, main_win, background, config, message,
+ get_colpair(config, 'main'))
return main_win, statusbar
From ee91bf193ca530a6c06f75ab08ae99f78abf7a51 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 06:16:31 +0100
Subject: [PATCH 0171/1650] Use bpython.pager.get_pager_command
And use shlex to split PAGER
Signed-off-by: Sebastian Ramacher
---
bpython/curtsiesfrontend/repl.py | 3 ++-
bpython/pager.py | 6 +++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py
index 42b988ec9..73e5427ac 100644
--- a/bpython/curtsiesfrontend/repl.py
+++ b/bpython/curtsiesfrontend/repl.py
@@ -34,6 +34,7 @@
from bpython import translations; translations.init()
from bpython.translations import _
from bpython._py3compat import py3
+from bpython.pager import get_pager_command
from bpython.curtsiesfrontend import replpainter as paint
from bpython.curtsiesfrontend import sitefix; sitefix.monkeypatch_quit()
@@ -1350,7 +1351,7 @@ def focus_on_subprocess(self, args):
signal.signal(signal.SIGWINCH, prev_sigwinch_handler)
def pager(self, text):
- command = os.environ.get('PAGER', 'less -rf').split()
+ command = get_pager_command()
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(text)
tmp.flush()
diff --git a/bpython/pager.py b/bpython/pager.py
index b397b2827..1a99e8e57 100644
--- a/bpython/pager.py
+++ b/bpython/pager.py
@@ -27,10 +27,10 @@
import pydoc
import subprocess
import sys
+import shlex
-
-def get_pager_command():
- command = os.environ.get('PAGER', 'less -r').split()
+def get_pager_command(default='less -r'):
+ command = shlex.split(os.environ.get('PAGER', default))
return command
From 2afccdd11cc827a932fbe53a95718c285093b9e1 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 07:58:23 +0100
Subject: [PATCH 0172/1650] Use pager for help browser (fixes #294)
Signed-off-by: Sebastian Ramacher
---
bpython/curtsies.py | 12 +++++++++
bpython/curtsiesfrontend/_internal.py | 39 +++++++++++++++++++++++++++
2 files changed, 51 insertions(+)
create mode 100644 bpython/curtsiesfrontend/_internal.py
diff --git a/bpython/curtsies.py b/bpython/curtsies.py
index e98b2fd2b..7d562a41c 100644
--- a/bpython/curtsies.py
+++ b/bpython/curtsies.py
@@ -105,6 +105,18 @@ def after_suspend():
after_suspend=after_suspend) as repl:
repl.height, repl.width = window.t.height, window.t.width
+ if interactive:
+ # Add custom help command
+ # TODO: add methods to run the code
+ repl.coderunner.interp.locals['_repl'] = repl
+
+ repl.coderunner.interp.runsource(
+ 'from bpython.curtsiesfrontend._internal import _Helper')
+ repl.coderunner.interp.runsource('help = _Helper(_repl)\n')
+
+ del repl.coderunner.interp.locals['_repl']
+ del repl.coderunner.interp.locals['_Helper']
+
def process_event(e):
"""If None is passed in, just paint the screen"""
try:
diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py
new file mode 100644
index 000000000..98c1d5c89
--- /dev/null
+++ b/bpython/curtsiesfrontend/_internal.py
@@ -0,0 +1,39 @@
+# The MIT License
+#
+# Copyright (c) 2015 the bpython authors.
+#
+# 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.
+
+import sys
+import pydoc
+
+import bpython._internal
+
+class _Helper(bpython._internal._Helper):
+
+ def __init__(self, repl=None):
+ self._repl = repl
+ pydoc.pager = self.pager
+
+ super(_Helper, self).__init__()
+
+ def pager(self, output):
+ self._repl.pager(output)
+
+# vim: sw=4 ts=4 sts=4 ai et
From 8b2ce14d3f37390cd177800ac87da540df3f0b56 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 20:34:37 +0100
Subject: [PATCH 0173/1650] Add 0.13.2 changelog
Signed-off-by: Sebastian Ramacher
(cherry picked from commit 27775bf3b995f6fbda746a8c37276bf5e4615905)
Conflicts:
doc/sphinx/source/changelog.rst
---
CHANGELOG | 10 +++++++++-
doc/sphinx/source/changelog.rst | 24 ++++++++++++++++++++++++
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG b/CHANGELOG
index 33a74bd5d..c789c5e9c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,15 @@
Changelog
=========
-v0.13.1
+0.13.2
+-------
+
+A bugfix release. The fixed bugs are:
+
+* #424: Use new JSON API at bpaste.net.
+* #430: Fixed SNI issues with new pastebin service on Mac OS X.
+
+0.13.1
-------
A bugfix release. The fixed bugs are:
diff --git a/doc/sphinx/source/changelog.rst b/doc/sphinx/source/changelog.rst
index 6a8f25bed..e766e56f2 100644
--- a/doc/sphinx/source/changelog.rst
+++ b/doc/sphinx/source/changelog.rst
@@ -1,6 +1,30 @@
Changelog
=========
+0.13.2
+------
+
+A bugfix release. The fixed bugs are:
+
+* #424: Use new JSON API at bpaste.net.
+* #430: Fixed SNI issues with new pastebin service on Mac OS X.
+
+0.13.1
+------
+
+A bugfix release. The fixed bugs are:
+
+* #287: Turned off dictionary completion in bpython-curtsies
+* #281: Fixed a crash on error-raising properties
+* #286: Fixed input in Python 3
+* #293: Added encoding attribute to stdin bpython curtsies
+* #296: Fixed warnings in import completion for Python 3
+* #290: Stop using root logger
+* #301: Specify curtsies version in requirements
+
+There's also a necessary regression: #232 (adding fileno() on stdin)
+is reintroduced because its previous fix was found to be the cause of #286
+
0.13
----
From 1509a5486610db9ad4fffacfe87e96890194ff8b Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Fri, 9 Jan 2015 21:07:19 +0100
Subject: [PATCH 0174/1650] Add #432 to changelog
Signed-off-by: Sebastian Ramacher
(cherry picked from commit b30971bd4f644150e845e9c35ffb22d6312b456d)
---
CHANGELOG | 2 ++
doc/sphinx/source/changelog.rst | 2 ++
2 files changed, 4 insertions(+)
diff --git a/CHANGELOG b/CHANGELOG
index c789c5e9c..68275fd4c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -8,6 +8,8 @@ A bugfix release. The fixed bugs are:
* #424: Use new JSON API at bpaste.net.
* #430: Fixed SNI issues with new pastebin service on Mac OS X.
+* #432: Fixed crash in bpython-curtsies in special circumstances if history file
+ is empty. Thanks to Lisa van Gelder.
0.13.1
-------
diff --git a/doc/sphinx/source/changelog.rst b/doc/sphinx/source/changelog.rst
index e766e56f2..da8b74e75 100644
--- a/doc/sphinx/source/changelog.rst
+++ b/doc/sphinx/source/changelog.rst
@@ -8,6 +8,8 @@ A bugfix release. The fixed bugs are:
* #424: Use new JSON API at bpaste.net.
* #430: Fixed SNI issues with new pastebin service on Mac OS X.
+* #432: Fixed crash in bpython-curtsies in special circumstances if history file
+ is empty. Thanks to Lisa van Gelder.
0.13.1
------
From f9c5e7d2e359edf0e2d1f1a45bdad987a7b0f525 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 17:38:44 +0100
Subject: [PATCH 0175/1650] Move History to seperate file and improve error
handling (fixes #434)
A new method append_reload_and_write is added that handles read and write
failures better.
Signed-off-by: Sebastian Ramacher
---
bpython/history.py | 223 +++++++++++++++++++++++++++++++++++++++++++++
bpython/repl.py | 168 +++-------------------------------
2 files changed, 236 insertions(+), 155 deletions(-)
create mode 100644 bpython/history.py
diff --git a/bpython/history.py b/bpython/history.py
new file mode 100644
index 000000000..48055999e
--- /dev/null
+++ b/bpython/history.py
@@ -0,0 +1,223 @@
+# The MIT License
+#
+# Copyright (c) 2009-2015 the bpython authors.
+#
+# 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.
+
+
+import codecs
+import os
+
+from bpython.translations import _
+
+
+class History(object):
+ """Stores readline-style history and current place in it"""
+
+ def __init__(self, entries=None, duplicates=True, hist_size=100):
+ if entries is None:
+ self.entries = ['']
+ else:
+ self.entries = list(entries)
+ self.index = 0 # how many lines back in history is currently selected
+ # where 0 is the saved typed line, 1 the prev entered line
+ self.saved_line = '' # what was on the prompt before using history
+ self.duplicates = duplicates
+ self.hist_size = hist_size
+
+
+ def append(self, line):
+ self.append_to(self.entries, line)
+
+
+ def append_to(self, entries, line):
+ line = line.rstrip('\n')
+ if line:
+ if not self.duplicates:
+ # remove duplicates
+ try:
+ while True:
+ entries.remove(line)
+ except ValueError:
+ pass
+ entries.append(line)
+
+
+ def first(self):
+ """Move back to the beginning of the history."""
+ if not self.is_at_end:
+ self.index = len(self.entries)
+ return self.entries[-self.index]
+
+
+ def back(self, start=True, search=False, target=None, include_current=False):
+ """Move one step back in the history."""
+ if target is None:
+ target = self.saved_line
+ if not self.is_at_end:
+ if search:
+ self.index += self.find_partial_match_backward(target, include_current)
+ elif start:
+ self.index += self.find_match_backward(target, include_current)
+ else:
+ self.index += 1
+ return self.entry
+
+
+ @property
+ def entry(self):
+ """The current entry, which may be the saved line"""
+ return self.entries[-self.index] if self.index else self.saved_line
+
+
+ @property
+ def entries_by_index(self):
+ return list(reversed(self.entries + [self.saved_line]))
+
+
+ def find_match_backward(self, search_term, include_current=False):
+ for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
+ if val.startswith(search_term):
+ return idx + (0 if include_current else 1)
+ return 0
+
+
+ def find_partial_match_backward(self, search_term, include_current=False):
+ for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
+ if search_term in val:
+ return idx + (0 if include_current else 1)
+ return 0
+
+
+ def forward(self, start=True, search=False, target=None, include_current=False):
+ """Move one step forward in the history."""
+ if target is None:
+ target = self.saved_line
+ if self.index > 1:
+ if search:
+ self.index -= self.find_partial_match_forward(target, include_current)
+ elif start:
+ self.index -= self.find_match_forward(target, include_current)
+ else:
+ self.index -= 1
+ return self.entry
+ else:
+ self.index = 0
+ return self.saved_line
+
+
+ def find_match_forward(self, search_term, include_current=False):
+ #TODO these are no longer efficient, because we realize the whole list. Does this matter?
+ for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
+ if val.startswith(search_term):
+ return idx + (0 if include_current else 1)
+ return self.index
+
+
+ def find_partial_match_forward(self, search_term, include_current=False):
+ for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
+ if search_term in val:
+ return idx + (0 if include_current else 1)
+ return self.index
+
+
+ def last(self):
+ """Move forward to the end of the history."""
+ if not self.is_at_start:
+ self.index = 0
+ return self.entries[0]
+
+
+ @property
+ def is_at_end(self):
+ return self.index >= len(self.entries) or self.index == -1
+
+
+ @property
+ def is_at_start(self):
+ return self.index == 0
+
+
+ def enter(self, line):
+ if self.index == 0:
+ self.saved_line = line
+
+
+ @classmethod
+ def from_filename(cls, filename):
+ history = cls()
+ history.load(filename)
+ return history
+
+
+ def reset(self):
+ self.index = 0
+ self.saved_line = ''
+
+
+ def load(self, filename, encoding):
+ with codecs.open(filename, 'r', encoding, 'ignore') as hfile:
+ self.entries = self.load_from(hfile)
+
+
+ def load_from(self, fd):
+ entries = []
+ for line in fd:
+ self.append_to(entries, line)
+ return entries
+
+
+ def save(self, filename, encoding, lines=0):
+ with codecs.open(filename, 'w', encoding, 'ignore') as hfile:
+ self.save_to(hfile, self.entries, lines)
+
+
+ def save_to(self, fd, entries=None, lines=0):
+ if entries is None:
+ entries = self.entries
+ for line in entries[-lines:]:
+ fd.write(line)
+ fd.write('\n')
+
+
+ def append_reload_and_write(self, s, filename, encoding):
+ if not self.hist_size:
+ return self.append(s)
+
+ try:
+ with codecs.open(filename, 'rw+', encoding, 'ignore') as hfile:
+ # read entries
+ hfile.seek(0, os.SEEK_SET)
+ entries = self.load_from(hfile)
+ self.append_to(entries, s)
+
+ # write new entries
+ hfile.seek(0, os.SEEK_SET)
+ hfile.truncate()
+ self.save_to(hfile, entries, self.hist_size)
+
+ self.entries = entries
+ except EnvironmentError as err:
+ raise RuntimeError(_('Error occurded while writing to file %s (%s)')
+ % (filename, err.strerror))
+ else:
+ if len(self.entries) == 0:
+ # Make sure that entries contains at least one element. If the
+ # file and s are empty, this can occur.
+ self.entries = ['']
diff --git a/bpython/repl.py b/bpython/repl.py
index 3d579818c..31c7d4b49 100644
--- a/bpython/repl.py
+++ b/bpython/repl.py
@@ -49,6 +49,7 @@
from bpython.formatter import Parenthesis
from bpython.translations import _
from bpython.clipboard import get_clipboard, CopyFailed
+from bpython.history import History
import bpython.autocomplete as autocomplete
@@ -137,143 +138,6 @@ def writetb(self, lines):
self.write(line)
-class History(object):
- """Stores readline-style history and current place in it"""
-
- def __init__(self, entries=None, duplicates=False):
- if entries is None:
- self.entries = ['']
- else:
- self.entries = list(entries)
- self.index = 0 # how many lines back in history is currently selected
- # where 0 is the saved typed line, 1 the prev entered line
- self.saved_line = '' # what was on the prompt before using history
- self.duplicates = duplicates
-
- def append(self, line):
- line = line.rstrip('\n')
- if line:
- if not self.duplicates:
- # remove duplicates
- try:
- while True:
- self.entries.remove(line)
- except ValueError:
- pass
- self.entries.append(line)
-
- def first(self):
- """Move back to the beginning of the history."""
- if not self.is_at_end:
- self.index = len(self.entries)
- return self.entries[-self.index]
-
- def back(self, start=True, search=False, target=None, include_current=False):
- """Move one step back in the history."""
- if target is None:
- target = self.saved_line
- if not self.is_at_end:
- if search:
- self.index += self.find_partial_match_backward(target, include_current)
- elif start:
- self.index += self.find_match_backward(target, include_current)
- else:
- self.index += 1
- return self.entry
-
- @property
- def entry(self):
- """The current entry, which may be the saved line"""
- return self.entries[-self.index] if self.index else self.saved_line
-
- @property
- def entries_by_index(self):
- return list(reversed(self.entries + [self.saved_line]))
-
- def find_match_backward(self, search_term, include_current=False):
- for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
- if val.startswith(search_term):
- return idx + (0 if include_current else 1)
- return 0
-
- def find_partial_match_backward(self, search_term, include_current=False):
- for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
- if search_term in val:
- return idx + (0 if include_current else 1)
- return 0
-
-
- def forward(self, start=True, search=False, target=None, include_current=False):
- """Move one step forward in the history."""
- if target is None:
- target = self.saved_line
- if self.index > 1:
- if search:
- self.index -= self.find_partial_match_forward(target, include_current)
- elif start:
- self.index -= self.find_match_forward(target, include_current)
- else:
- self.index -= 1
- return self.entry
- else:
- self.index = 0
- return self.saved_line
-
- def find_match_forward(self, search_term, include_current=False):
- #TODO these are no longer efficient, because we realize the whole list. Does this matter?
- for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
- if val.startswith(search_term):
- return idx + (0 if include_current else 1)
- return self.index
-
- def find_partial_match_forward(self, search_term, include_current=False):
- for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
- if search_term in val:
- return idx + (0 if include_current else 1)
- return self.index
-
-
-
- def last(self):
- """Move forward to the end of the history."""
- if not self.is_at_start:
- self.index = 0
- return self.entries[0]
-
- @property
- def is_at_end(self):
- return self.index >= len(self.entries) or self.index == -1
-
- @property
- def is_at_start(self):
- return self.index == 0
-
- def enter(self, line):
- if self.index == 0:
- self.saved_line = line
-
- @classmethod
- def from_filename(cls, filename):
- history = cls()
- history.load(filename)
- return history
-
- def load(self, filename, encoding):
- with codecs.open(filename, 'r', encoding, 'ignore') as hfile:
- for line in hfile:
- self.append(line)
-
- def reset(self):
- self.index = 0
- self.saved_line = ''
-
- def save(self, filename, encoding, lines=0):
- with codecs.open(filename, 'w', encoding, 'ignore') as hfile:
- for line in self.entries[-lines:]:
- hfile.write(line)
- hfile.write('\n')
-
-
class MatchesIterator(object):
"""Stores a list of matches and which one is currently selected if any.
@@ -429,7 +293,8 @@ def __init__(self, interp, config):
self.interp = interp
self.interp.syntaxerror_callback = self.clear_current_line
self.match = False
- self.rl_history = History(duplicates=config.hist_duplicates)
+ self.rl_history = History(duplicates=config.hist_duplicates,
+ hist_size=config.hist_length)
self.s_hist = []
self.history = []
self.evaluating = False
@@ -451,8 +316,11 @@ def __init__(self, interp, config):
pythonhist = os.path.expanduser(self.config.hist_file)
if os.path.exists(pythonhist):
- self.rl_history.load(pythonhist,
- getpreferredencoding() or "ascii")
+ try:
+ self.rl_history.load(pythonhist,
+ getpreferredencoding() or "ascii")
+ except EnvironmentError:
+ pass
@property
def ps1(self):
@@ -905,21 +773,11 @@ def push(self, s, insert_into_history=True):
return more
def insert_into_history(self, s):
- if self.config.hist_length:
- histfilename = self.config.hist_file
- oldhistory = self.rl_history.entries
- self.rl_history.entries = []
- if os.path.exists(histfilename):
- self.rl_history.load(histfilename, getpreferredencoding())
- self.rl_history.append(s)
- try:
- self.rl_history.save(histfilename, getpreferredencoding(), self.config.hist_length)
- except EnvironmentError as err:
- self.interact.notify("Error occured while writing to file %s (%s) " % (histfilename, err.strerror))
- self.rl_history.entries = oldhistory
- self.rl_history.append(s)
- else:
- self.rl_history.append(s)
+ try:
+ self.rl_history.append_reload_and_write(s, self.config.hist_file,
+ getpreferredencoding())
+ except RuntimeError as e:
+ self.interact.notify(str(e))
def undo(self, n=1):
"""Go back in the undo history n steps and call reeavluate()
From 0b626d4aeac02fe6837fd5493d0e9c172b480071 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 17:41:16 +0100
Subject: [PATCH 0176/1650] Add file locking for history file
Signed-off-by: Sebastian Ramacher
---
bpython/filelock.py | 50 +++++++++++++++++++++++++++++++++++++++++++++
bpython/history.py | 30 +++++++++++++++------------
2 files changed, 67 insertions(+), 13 deletions(-)
create mode 100644 bpython/filelock.py
diff --git a/bpython/filelock.py b/bpython/filelock.py
new file mode 100644
index 000000000..532cb4b7c
--- /dev/null
+++ b/bpython/filelock.py
@@ -0,0 +1,50 @@
+# The MIT License
+#
+# Copyright (c) 2015 the bpython authors.
+#
+# 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.
+
+
+try:
+ import fcntl
+ has_fcntl = True
+except ImportError:
+ has_fcntl = False
+
+
+class FileLock(object):
+ """Simple file locking
+
+ On platforms without fcntl, all operations in this class are no-ops.
+ """
+
+ def __init__(self, fd, mode=fcntl.LOCK_EX):
+ self.fd = fd
+ self.mode = mode
+
+ def __enter__(self):
+ if has_fcntl:
+ fcntl.flock(self.fd, self.mode)
+ return self
+
+ def __exit__(self, *args):
+ if has_fcntl:
+ fcntl.flock(self.fd, fcntl.LOCK_UN)
+
+# vim: sw=4 ts=4 sts=4 ai et
diff --git a/bpython/history.py b/bpython/history.py
index 48055999e..1ff58e75e 100644
--- a/bpython/history.py
+++ b/bpython/history.py
@@ -25,6 +25,7 @@
import os
from bpython.translations import _
+from bpython.filelock import FileLock
class History(object):
@@ -173,7 +174,8 @@ def reset(self):
def load(self, filename, encoding):
with codecs.open(filename, 'r', encoding, 'ignore') as hfile:
- self.entries = self.load_from(hfile)
+ with FileLock(hfile) as lock:
+ self.entries = self.load_from(hfile)
def load_from(self, fd):
@@ -185,7 +187,8 @@ def load_from(self, fd):
def save(self, filename, encoding, lines=0):
with codecs.open(filename, 'w', encoding, 'ignore') as hfile:
- self.save_to(hfile, self.entries, lines)
+ with FileLock(hfile) as lock:
+ self.save_to(hfile, self.entries, lines)
def save_to(self, fd, entries=None, lines=0):
@@ -202,17 +205,18 @@ def append_reload_and_write(self, s, filename, encoding):
try:
with codecs.open(filename, 'rw+', encoding, 'ignore') as hfile:
- # read entries
- hfile.seek(0, os.SEEK_SET)
- entries = self.load_from(hfile)
- self.append_to(entries, s)
-
- # write new entries
- hfile.seek(0, os.SEEK_SET)
- hfile.truncate()
- self.save_to(hfile, entries, self.hist_size)
-
- self.entries = entries
+ with FileLock(hfile) as lock:
+ # read entries
+ hfile.seek(0, os.SEEK_SET)
+ entries = self.load_from(hfile)
+ self.append_to(entries, s)
+
+ # write new entries
+ hfile.seek(0, os.SEEK_SET)
+ hfile.truncate()
+ self.save_to(hfile, entries, self.hist_size)
+
+ self.entries = entries
except EnvironmentError as err:
raise RuntimeError(_('Error occurded while writing to file %s (%s)')
% (filename, err.strerror))
From 254aeaf767127a828c579c684d1d4cddc8fc5463 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 18:02:49 +0100
Subject: [PATCH 0177/1650] Use correct file mode
Signed-off-by: Sebastian Ramacher
---
bpython/history.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bpython/history.py b/bpython/history.py
index 1ff58e75e..9a40d3cdd 100644
--- a/bpython/history.py
+++ b/bpython/history.py
@@ -204,7 +204,7 @@ def append_reload_and_write(self, s, filename, encoding):
return self.append(s)
try:
- with codecs.open(filename, 'rw+', encoding, 'ignore') as hfile:
+ with codecs.open(filename, 'a+', encoding, 'ignore') as hfile:
with FileLock(hfile) as lock:
# read entries
hfile.seek(0, os.SEEK_SET)
From 22cb3caa562c1f0ee2578049f149803f5fe0b03c Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 22:00:18 +0100
Subject: [PATCH 0178/1650] EOL 80
Signed-off-by: Sebastian Ramacher
---
bpython/history.py | 12 ++++++++----
1 file changed, 8 insertions(+), 4 deletions(-)
diff --git a/bpython/history.py b/bpython/history.py
index 9a40d3cdd..5771444c6 100644
--- a/bpython/history.py
+++ b/bpython/history.py
@@ -37,7 +37,8 @@ def __init__(self, entries=None, duplicates=True, hist_size=100):
else:
self.entries = list(entries)
self.index = 0 # how many lines back in history is currently selected
- # where 0 is the saved typed line, 1 the prev entered line
+ # where 0 is the saved typed line, 1 the prev entered
+ # line
self.saved_line = '' # what was on the prompt before using history
self.duplicates = duplicates
self.hist_size = hist_size
@@ -67,13 +68,15 @@ def first(self):
return self.entries[-self.index]
- def back(self, start=True, search=False, target=None, include_current=False):
+ def back(self, start=True, search=False, target=None,
+ include_current=False):
"""Move one step back in the history."""
if target is None:
target = self.saved_line
if not self.is_at_end:
if search:
- self.index += self.find_partial_match_backward(target, include_current)
+ self.index += self.find_partial_match_backward(target,
+ include_current)
elif start:
self.index += self.find_match_backward(target, include_current)
else:
@@ -106,7 +109,8 @@ def find_partial_match_backward(self, search_term, include_current=False):
return 0
- def forward(self, start=True, search=False, target=None, include_current=False):
+ def forward(self, start=True, search=False, target=None,
+ include_current=False):
"""Move one step forward in the history."""
if target is None:
target = self.saved_line
From 35cb7c449053ab872b570ed26183e073a823ec0b Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher
Date: Sun, 11 Jan 2015 22:00:58 +0100
Subject: [PATCH 0179/1650] Do not realize the whole entry list
Signed-off-by: Sebastian Ramacher
---
bpython/history.py | 29 ++++++++++++++++++++---------
1 file changed, 20 insertions(+), 9 deletions(-)
diff --git a/bpython/history.py b/bpython/history.py
index 5771444c6..1ce144d57 100644
--- a/bpython/history.py
+++ b/bpython/history.py
@@ -23,6 +23,7 @@
import codecs
import os
+from itertools import islice
from bpython.translations import _
from bpython.filelock import FileLock
@@ -96,16 +97,20 @@ def entries_by_index(self):
def find_match_backward(self, search_term, include_current=False):
- for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
+ add = 0 if include_current else 1
+ start = self.index + add
+ for idx, val in enumerate(islice(self.entries_by_index, start, None)):
if val.startswith(search_term):
- return idx + (0 if include_current else 1)
+ return idx + add
return 0
def find_partial_match_backward(self, search_term, include_current=False):
- for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
+ add = 0 if include_current else 1
+ start = self.index + add
+ for idx, val in enumerate(islice(self.entries_by_index, start, None)):
if search_term in val:
- return idx + (0 if include_current else 1)
+ return idx + add
return 0
@@ -116,7 +121,8 @@ def forward(self, start=True, search=False, target=None,
target = self.saved_line
if self.index > 1:
if search:
- self.index -= self.find_partial_match_forward(target, include_current)
+ self.index -= self.find_partial_match_forward(target,
+ include_current)
elif start:
self.index -= self.find_match_forward(target, include_current)
else:
@@ -128,17 +134,22 @@ def forward(self, start=True, search=False, target=None,
def find_match_forward(self, search_term, include_current=False):
- #TODO these are no longer efficient, because we realize the whole list. Does this matter?
- for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
+ add = 0 if include_current else 1
+ end = max(0, self.index - (1 - add))
+ for idx in xrange(end):
+ val = self.entries_by_index[end - 1 - idx]
if val.startswith(search_term):
return idx + (0 if include_current else 1)
return self.index
def find_partial_match_forward(self, search_term, include_current=False):
- for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
+ add = 0 if include_current else 1
+ end = max(0, self.index - (1 - add))
+ for idx in xrange(end):
+ val = self.entries_by_index[end - 1 - idx]
if search_term in val:
- return idx + (0 if include_current else 1)
+ return idx + add
return self.index
From a8be1281f9651b6d52feef03afffeaa5aa432692 Mon Sep 17 00:00:00 2001
From: Sebastian Ramacher