X Tutup
Skip to content

Commit 5b1143e

Browse files
committed
Merge all changes in fix_384
2 parents a4552db + 73d74b2 commit 5b1143e

File tree

15 files changed

+209
-86
lines changed

15 files changed

+209
-86
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ python:
88

99
install:
1010
- "python setup.py install"
11-
- "pip install curtsies greenlet pygments"
11+
- "pip install curtsies greenlet pygments requests"
1212

1313
script:
1414
- cd build/lib/

bpython/cli.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -974,10 +974,10 @@ def p_key(self, key):
974974
source = format(PythonLexer().get_tokens(source),
975975
TerminalFormatter())
976976
page(source)
977-
except (ValueError, NameError), e:
977+
except (ValueError, AttributeError, IOError, TypeError), e:
978978
self.statusbar.message(_(e))
979-
except (AttributeError, IOError, TypeError), e:
980-
self.statusbar.message(_('Failed to get source: %s' % e))
979+
except (NameError), e:
980+
self.statusbar.message(_('Cannot get source: %s' % e))
981981
return ''
982982

983983
elif key in ('\n', '\r', 'PADENTER'):
@@ -1001,6 +1001,9 @@ def p_key(self, key):
10011001
elif key == '\x18':
10021002
return self.send_current_line_to_editor()
10031003

1004+
elif key == '\x03':
1005+
raise KeyboardInterrupt()
1006+
10041007
elif key[0:3] == 'PAD' and not key in ('PAD0', 'PADSTOP'):
10051008
pad_keys = {
10061009
'PADMINUS': '-',

bpython/config.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ def loadini(struct, configfile):
5252
'syntax': True,
5353
'tab_length': 4,
5454
'pastebin_confirm': True,
55-
'pastebin_private': False,
56-
'pastebin_url': 'http://bpaste.net/xmlrpc/',
57-
'pastebin_private': True,
58-
'pastebin_show_url': 'http://bpaste.net/show/$paste_id/',
55+
'pastebin_url': 'https://bpaste.net/json/new',
56+
'pastebin_show_url': 'https://bpaste.net/show/$paste_id',
57+
'pastebin_removal_url': 'https://bpaste.net/remove/$removal_id',
58+
'pastebin_expiry': '1week',
5959
'pastebin_helper': '',
6060
'save_append_py': False,
6161
'editor': os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
@@ -140,10 +140,10 @@ def loadini(struct, configfile):
140140
struct.help_key = config.get('keyboard', 'help')
141141

142142
struct.pastebin_confirm = config.getboolean('general', 'pastebin_confirm')
143-
struct.pastebin_private = config.getboolean('general', 'pastebin_private')
144143
struct.pastebin_url = config.get('general', 'pastebin_url')
145-
struct.pastebin_private = config.get('general', 'pastebin_private')
146144
struct.pastebin_show_url = config.get('general', 'pastebin_show_url')
145+
struct.pastebin_removal_url = config.get('general', 'pastebin_removal_url')
146+
struct.pastebin_expiry = config.get('general', 'pastebin_expiry')
147147
struct.pastebin_helper = config.get('general', 'pastebin_helper')
148148

149149
struct.cli_suggestion_width = config.getfloat('cli',

bpython/curtsiesfrontend/manual_readline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def titlecase_next_word(cursor_offset, line):
268268
def delete_word_from_cursor_back(cursor_offset, line):
269269
"""Whatever my option-delete does in bash on my mac"""
270270
if not line:
271-
return cursor_offset, line
271+
return cursor_offset, line, ''
272272
starts = [m.start() for m in list(re.finditer(r'\b\w', line)) if m.start() < cursor_offset]
273273
if starts:
274274
return starts[-1], line[:starts[-1]] + line[cursor_offset:], line[starts[-1]:cursor_offset]

bpython/curtsiesfrontend/repl.py

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
#TODO other autocomplete modes (also fix in other bpython implementations)
4848

49+
4950
from curtsies.configfile_keynames import keymap as key_dispatch
5051

5152
logger = logging.getLogger(__name__)
@@ -87,6 +88,7 @@ def __init__(self, coderunner, repl, configured_edit_keys=None):
8788

8889
def process_event(self, e):
8990
assert self.has_focus
91+
9092
logger.debug('fake input processing event %r', e)
9193
if isinstance(e, events.PasteEvent):
9294
for ee in e.events:
@@ -100,6 +102,9 @@ def process_event(self, e):
100102
self.current_line = ''
101103
self.cursor_offset = 0
102104
self.repl.run_code_and_maybe_finish()
105+
elif e in ("<Esc+.>",):
106+
self.get_last_word()
107+
103108
elif e in ["<ESC>"]:
104109
pass
105110
elif e in ['<Ctrl-d>']:
@@ -469,6 +474,8 @@ def process_key_event(self, e):
469474
self.down_one_line()
470475
elif e in ("<Ctrl-d>",):
471476
self.on_control_d()
477+
elif e in ("<Esc+.>",):
478+
self.get_last_word()
472479
elif e in ("<Esc+r>",):
473480
self.incremental_search(reverse=True)
474481
elif e in ("<Esc+s>",):
@@ -524,6 +531,21 @@ def process_key_event(self, e):
524531
else:
525532
self.add_normal_character(e)
526533

534+
def get_last_word(self):
535+
536+
def last_word(line):
537+
if not line:
538+
return ''
539+
return line.split().pop()
540+
541+
previous_word = last_word(self.rl_history.entry)
542+
word = last_word(self.rl_history.back())
543+
line=self.current_line
544+
self._set_current_line(line[:len(line)-len(previous_word)]+word, reset_rl_history=False)
545+
546+
self._set_cursor_offset(self.cursor_offset-len(previous_word)+len(word), reset_rl_history=False)
547+
548+
527549
def incremental_search(self, reverse=False, include_current=False):
528550
if self.special_mode == None:
529551
self.rl_history.enter(self.current_line)
@@ -735,6 +757,19 @@ def update_completion(self, tab=False):
735757
self.current_match = None
736758
self.list_win_visible = BpythonRepl.complete(self, tab)
737759

760+
def predicted_indent(self, line):
761+
#TODO get rid of this! It's repeated code! Combine with Repl.
762+
logger.debug('line is %r', line)
763+
indent = len(re.match(r'[ ]*', line).group())
764+
if line.endswith(':'):
765+
indent = max(0, indent + self.config.tab_length)
766+
elif line and line.count(' ') == len(line):
767+
indent = max(0, indent - self.config.tab_length)
768+
elif line and ':' not in line and line.strip().startswith(('return', 'pass', 'raise', 'yield')):
769+
indent = max(0, indent - self.config.tab_length)
770+
logger.debug('indent we found was %s', indent)
771+
return indent
772+
738773
def push(self, line, insert_into_history=True):
739774
"""Push a line of code onto the buffer, start running the buffer
740775
@@ -743,14 +778,7 @@ def push(self, line, insert_into_history=True):
743778
if self.paste_mode:
744779
self.saved_indent = 0
745780
else:
746-
indent = len(re.match(r'[ ]*', line).group())
747-
if line.endswith(':'):
748-
indent = max(0, indent + self.config.tab_length)
749-
elif line and line.count(' ') == len(line):
750-
indent = max(0, indent - self.config.tab_length)
751-
elif line and ':' not in line and line.strip().startswith(('return', 'pass', 'raise', 'yield')):
752-
indent = max(0, indent - self.config.tab_length)
753-
self.saved_indent = indent
781+
self.saved_indent = self.predicted_indent(line)
754782

755783
#current line not added to display buffer if quitting #TODO I don't understand this comment
756784
if self.config.syntax:
@@ -808,7 +836,8 @@ def run_code_and_maybe_finish(self, for_code=None):
808836
if err:
809837
indent = 0
810838

811-
#TODO This should be printed ABOVE the error that just happened instead
839+
840+
#TODO This should be printed ABOVE the error that just happened instead
812841
# or maybe just thrown away and not shown
813842
if self.current_stdouterr_line:
814843
self.display_lines.extend(paint.display_linize(self.current_stdouterr_line, self.width))
@@ -1193,6 +1222,20 @@ def reprint_line(self, lineno, tokens):
11931222
logger.debug("calling reprint line with %r %r", lineno, tokens)
11941223
if self.config.syntax:
11951224
self.display_buffer[lineno] = bpythonparse(format(tokens, self.formatter))
1225+
1226+
def take_back_buffer_line(self):
1227+
self.display_buffer.pop()
1228+
self.buffer.pop()
1229+
1230+
if not self.buffer:
1231+
self.current_line = ''
1232+
self.cursor_offset = 0
1233+
else:
1234+
line = self.buffer[-1]
1235+
indent = self.predicted_indent(line)
1236+
self.current_line = indent * ' '
1237+
self.cursor_offset = len(self.current_line)
1238+
11961239
def reevaluate(self, insert_into_history=False):
11971240
"""bpython.Repl.undo calls this"""
11981241
if self.watcher: self.watcher.reset()
@@ -1257,14 +1300,17 @@ def pager(self, text):
12571300
self.focus_on_subprocess(command + [tmp.name])
12581301

12591302
def show_source(self):
1260-
source = self.get_source_of_current_name()
1261-
if source is None:
1262-
self.status_bar.message(_('Cannot show source.'))
1263-
else:
1303+
try:
1304+
source = self.get_source_of_current_name()
12641305
if self.config.highlight_show_source:
1265-
source = format(PythonLexer().get_tokens(source), TerminalFormatter())
1306+
source = format(PythonLexer().get_tokens(source),
1307+
TerminalFormatter())
12661308
self.pager(source)
1267-
1309+
except (ValueError, AttributeError, IOError, TypeError), e:
1310+
self.status_bar.message(_(e))
1311+
except (NameError), e:
1312+
self.status_bar.message(_('Cannot get source: %s' % e))
1313+
12681314
def help_text(self):
12691315
return (self.version_help_text() + '\n' + self.key_help_text()).encode('utf8')
12701316

bpython/repl.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import logging
3030
import os
3131
import pydoc
32+
import requests
3233
import shlex
3334
import subprocess
3435
import sys
@@ -41,8 +42,7 @@
4142
from socket import error as SocketError
4243
from string import Template
4344
from urllib import quote as urlquote
44-
from urlparse import urlparse
45-
from xmlrpclib import ServerProxy, Error as XMLRPCError
45+
from urlparse import urlparse, urljoin
4646

4747
from pygments.token import Token
4848

@@ -598,9 +598,14 @@ def get_source_of_current_name(self):
598598
raise ValueError("Cannot get source of an empty string")
599599
if inspection.is_eval_safe_name(line):
600600
obj = self.get_object(line)
601-
if obj is None:
602-
raise NameError("%s is not defined" % line)
603-
return inspect.getsource(obj) #throws an exception that we'll catch
601+
try:
602+
inspect.getsource(obj)
603+
except TypeError, e:
604+
msg = e.message
605+
if "built-in" in msg:
606+
raise TypeError("Cannot access source of <built-in function %s>" % self.current_line)
607+
else:
608+
raise TypeError("No source code found for %s" % self.current_line)
604609

605610
def set_docstring(self):
606611
self.docstring = None
@@ -780,32 +785,41 @@ def do_pastebin(self, s):
780785
if self.config.pastebin_helper:
781786
return self.do_pastebin_helper(s)
782787
else:
783-
return self.do_pastebin_xmlrpc(s)
788+
return self.do_pastebin_json(s)
784789

785-
def do_pastebin_xmlrpc(self, s):
786-
"""Upload to pastebin via XML-RPC."""
787-
try:
788-
pasteservice = ServerProxy(self.config.pastebin_url)
789-
except IOError, e:
790-
self.interact.notify(_("Pastebin error for URL '%s': %s") %
791-
(self.config.pastebin_url, str(e)))
792-
return
790+
def do_pastebin_json(self, s):
791+
"""Upload to pastebin via json interface."""
792+
793+
url = urljoin(self.config.pastebin_url, '/json/new')
794+
payload = {
795+
'code': s,
796+
'lexer': 'pycon',
797+
'expiry': self.config.pastebin_expiry
798+
}
793799

794800
self.interact.notify(_('Posting data to pastebin...'))
795801
try:
796-
paste_id = pasteservice.pastes.newPaste('pycon', s, '', '', '',
797-
self.config.pastebin_private)
798-
except (SocketError, XMLRPCError), e:
799-
self.interact.notify(_('Upload failed: %s') % (str(e), ) )
800-
return
802+
response = requests.post(url, data=payload, verify=True)
803+
response.raise_for_status()
804+
except requests.exceptions.RequestException as exc:
805+
self.interact.notify(_('Upload failed: %s') % (str(exc), ))
806+
return
801807

802808
self.prev_pastebin_content = s
809+
data = response.json()
803810

804811
paste_url_template = Template(self.config.pastebin_show_url)
805-
paste_id = urlquote(paste_id)
812+
paste_id = urlquote(data['paste_id'])
806813
paste_url = paste_url_template.safe_substitute(paste_id=paste_id)
814+
815+
removal_url_template = Template(self.config.pastebin_removal_url)
816+
removal_id = urlquote(data['removal_id'])
817+
removal_url = removal_url_template.safe_substitute(removal_id=removal_id)
818+
807819
self.prev_pastebin_url = paste_url
808-
self.interact.notify(_('Pastebin URL: %s') % (paste_url, ), 10)
820+
self.interact.notify(_('Pastebin URL: %s - Removal URL: %s') %
821+
(paste_url, removal_url))
822+
809823
return paste_url
810824

811825
def do_pastebin_helper(self, s):
@@ -899,8 +913,11 @@ def undo(self, n=1):
899913
entries = list(self.rl_history.entries)
900914

901915
self.history = self.history[:-n]
902-
903-
self.reevaluate()
916+
if (n == 1 and self.buffer and
917+
hasattr(self, 'take_back_buffer_line')):
918+
self.take_back_buffer_line()
919+
else:
920+
self.reevaluate()
904921

905922
self.rl_history.entries = entries
906923

bpython/test/test_curtsies_repl.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ def test_external_communication(self):
5151
self.repl.send_current_block_to_external_editor()
5252
self.repl.send_session_to_external_editor()
5353

54+
def test_get_last_word(self):
55+
self.repl.rl_history.entries=['1','2 3','4 5 6']
56+
self.repl._set_current_line('abcde')
57+
self.repl.get_last_word()
58+
self.assertEqual(self.repl.current_line,'abcde6')
59+
self.repl.get_last_word()
60+
self.assertEqual(self.repl.current_line,'abcde3')
61+
62+
@skip # this is the behavior of bash - not currently implemented
63+
def test_get_last_word_with_prev_line(self):
64+
self.repl.rl_history.entries=['1','2 3','4 5 6']
65+
self.repl._set_current_line('abcde')
66+
self.repl.up_one_line()
67+
self.assertEqual(self.repl.current_line,'4 5 6')
68+
self.repl.get_last_word()
69+
self.assertEqual(self.repl.current_line,'4 5 63')
70+
self.repl.get_last_word()
71+
self.assertEqual(self.repl.current_line,'4 5 64')
72+
self.repl.up_one_line()
73+
self.assertEqual(self.repl.current_line,'2 3')
74+
5475
@contextmanager # from http://stackoverflow.com/a/17981937/398212 - thanks @rkennedy
5576
def captured_output():
5677
new_out, new_err = StringIO(), StringIO()
@@ -93,6 +114,22 @@ def test_interactive(self):
93114

94115
self.assertEqual(out.getvalue(), '0.5\n0.5\n')
95116

117+
class TestPredictedIndent(unittest.TestCase):
118+
def setUp(self):
119+
self.repl = create_repl()
120+
121+
def test_simple(self):
122+
self.assertEqual(self.repl.predicted_indent(''), 0)
123+
self.assertEqual(self.repl.predicted_indent('class Foo:'), 4)
124+
self.assertEqual(self.repl.predicted_indent('class Foo: pass'), 0)
125+
self.assertEqual(self.repl.predicted_indent('def asdf():'), 4)
126+
self.assertEqual(self.repl.predicted_indent('def asdf(): return 7'), 0)
127+
128+
@skip
129+
def test_complex(self):
130+
self.assertEqual(self.repl.predicted_indent('[a,'), 1)
131+
self.assertEqual(self.repl.predicted_indent('reduce(asdfasdf,'), 7)
132+
96133

97134
if __name__ == '__main__':
98135
unittest.main()

0 commit comments

Comments
 (0)
X Tutup