Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 64d875e

Browse filesBrowse files
Merge branch 'paste-detection'
2 parents 2a9b51f + fa08b74 commit 64d875e
Copy full SHA for 64d875e

4 files changed

+192-13Lines changed: 192 additions & 13 deletions

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎bpython/curtsies.py‎

Copy file name to clipboardExpand all lines: bpython/curtsies.py
+41-4Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22

33
import code
4+
import collections
45
import io
56
import logging
67
import sys
@@ -91,10 +92,45 @@ def main(args=None, locals_=None, banner=None, welcome_message=None):
9192
return extract_exit_value(exit_value)
9293

9394

95+
def _combined_events(event_provider, paste_threshold):
96+
"""Combines consecutive keypress events into paste events."""
97+
timeout = yield 'nonsense_event' # so send can be used immediately
98+
queue = collections.deque()
99+
while True:
100+
e = event_provider.send(timeout)
101+
if isinstance(e, curtsies.events.Event):
102+
timeout = yield e
103+
continue
104+
elif e is None:
105+
timeout = yield None
106+
continue
107+
else:
108+
queue.append(e)
109+
e = event_provider.send(0)
110+
while not (e is None or isinstance(e, curtsies.events.Event)):
111+
queue.append(e)
112+
e = event_provider.send(0)
113+
if len(queue) >= paste_threshold:
114+
paste = curtsies.events.PasteEvent()
115+
paste.events.extend(queue)
116+
queue.clear()
117+
timeout = yield paste
118+
else:
119+
while len(queue):
120+
timeout = yield queue.popleft()
121+
122+
123+
def combined_events(event_provider, paste_threshold=3):
124+
g = _combined_events(event_provider, paste_threshold)
125+
next(g)
126+
return g
127+
128+
94129
def mainloop(config, locals_, banner, interp=None, paste=None,
95130
interactive=True):
96-
with curtsies.input.Input(keynames='curtsies', sigint_event=True) as \
97-
input_generator:
131+
with curtsies.input.Input(keynames='curtsies',
132+
sigint_event=True,
133+
paste_threshold=None) as input_generator:
98134
with curtsies.window.CursorAwareWindow(
99135
sys.stdout,
100136
sys.stdin,
@@ -180,12 +216,13 @@ def process_event(e):
180216

181217
# do a display before waiting for first event
182218
process_event(None)
219+
inputs = combined_events(input_generator)
183220
for unused in find_iterator:
184-
e = input_generator.send(0)
221+
e = inputs.send(0)
185222
if e is not None:
186223
process_event(e)
187224

188-
for e in input_generator:
225+
for e in inputs:
189226
process_event(e)
190227

191228

Collapse file

‎bpython/curtsiesfrontend/repl.py‎

Copy file name to clipboardExpand all lines: bpython/curtsiesfrontend/repl.py
+34-9Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@
8484
Press {config.edit_config_key} to edit this config file.
8585
"""
8686
EXAMPLE_CONFIG_URL = 'https://raw.githubusercontent.com/bpython/bpython/master/bpython/sample-config'
87+
MAX_EVENTS_POSSIBLY_NOT_PASTE = 20 # more than this many events will be assumed to
88+
# be a true paste event, i.e. control characters
89+
# like '<Ctrl-a>' will be stripped
8790

8891
# This is needed for is_nop and should be removed once is_nop is fixed.
8992
if py3:
@@ -545,16 +548,25 @@ def process_control_event(self, e):
545548
ctrl_char = compress_paste_event(e)
546549
if ctrl_char is not None:
547550
return self.process_event(ctrl_char)
548-
simple_events = just_simple_events(e.events)
549-
source = preprocess(''.join(simple_events),
550-
self.interp.compile)
551-
552551
with self.in_paste_mode():
553-
for ee in source:
554-
if self.stdin.has_focus:
555-
self.stdin.process_event(ee)
556-
else:
557-
self.process_simple_keypress(ee)
552+
# Might not really be a paste, UI might just be lagging
553+
if (len(e.events) <= MAX_EVENTS_POSSIBLY_NOT_PASTE and
554+
any(not is_simple_event(ee) for ee in e.events)):
555+
for ee in e.events:
556+
if self.stdin.has_focus:
557+
self.stdin.process_event(ee)
558+
else:
559+
self.process_event(ee)
560+
else:
561+
simple_events = just_simple_events(e.events)
562+
source = preprocess(''.join(simple_events),
563+
self.interp.compile)
564+
for ee in source:
565+
if self.stdin.has_focus:
566+
self.stdin.process_event(ee)
567+
else:
568+
self.process_simple_keypress(ee)
569+
558570

559571
elif isinstance(e, bpythonevents.RunStartupFileEvent):
560572
try:
@@ -1619,11 +1631,24 @@ def just_simple_events(event_list):
16191631
pass # ignore events
16201632
elif e == '<SPACE>':
16211633
simple_events.append(' ')
1634+
elif len(e) > 1:
1635+
pass # get rid of <Ctrl-a> etc.
16221636
else:
16231637
simple_events.append(e)
16241638
return simple_events
16251639

16261640

1641+
def is_simple_event(e):
1642+
if isinstance(e, events.Event):
1643+
return False
1644+
if e in ("<Ctrl-j>", "<Ctrl-m>", "<PADENTER>", "\n", "\r", "<SPACE>"):
1645+
return True
1646+
if len(e) > 1:
1647+
return False
1648+
else:
1649+
return True
1650+
1651+
16271652
# TODO this needs some work to function again and be useful for embedding
16281653
def simple_repl():
16291654
refreshes = []
Collapse file

‎bpython/test/test_curtsies.py‎

Copy file name to clipboard
+91Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# coding: utf-8
2+
from __future__ import unicode_literals
3+
4+
from collections import namedtuple
5+
6+
from bpython.curtsies import combined_events
7+
from bpython.test import (FixLanguageTestCase as TestCase, unittest)
8+
9+
import curtsies.events
10+
11+
12+
ScheduledEvent = namedtuple('ScheduledEvent', ['when', 'event'])
13+
14+
15+
class EventGenerator(object):
16+
def __init__(self, initial_events=(), scheduled_events=()):
17+
self._events = []
18+
self._current_tick = 0
19+
for e in initial_events:
20+
self.schedule_event(e, 0)
21+
for e, w in scheduled_events:
22+
self.schedule_event(e, w)
23+
24+
def schedule_event(self, event, when):
25+
self._events.append(ScheduledEvent(when, event))
26+
self._events.sort()
27+
28+
def send(self, timeout=None):
29+
if timeout not in [None, 0]:
30+
raise ValueError('timeout value %r not supported' % timeout)
31+
if not self._events:
32+
return None
33+
if self._events[0].when <= self._current_tick:
34+
return self._events.pop(0).event
35+
36+
if timeout == 0:
37+
return None
38+
elif timeout is None:
39+
e = self._events.pop(0)
40+
self._current_tick = e.when
41+
return e.event
42+
else:
43+
raise ValueError('timeout value %r not supported' % timeout)
44+
45+
def tick(self, dt=1):
46+
self._current_tick += dt
47+
return self._current_tick
48+
49+
50+
class TestCurtsiesPasteDetection(TestCase):
51+
def test_paste_threshold(self):
52+
eg = EventGenerator(list('abc'))
53+
cb = combined_events(eg, paste_threshold=3)
54+
e = next(cb)
55+
self.assertIsInstance(e, curtsies.events.PasteEvent)
56+
self.assertEqual(e.events, list('abc'))
57+
self.assertEqual(next(cb), None)
58+
59+
eg = EventGenerator(list('abc'))
60+
cb = combined_events(eg, paste_threshold=4)
61+
self.assertEqual(next(cb), 'a')
62+
self.assertEqual(next(cb), 'b')
63+
self.assertEqual(next(cb), 'c')
64+
self.assertEqual(next(cb), None)
65+
66+
def test_set_timeout(self):
67+
eg = EventGenerator('a', zip('bcdefg', [1, 2, 3, 3, 3, 4]))
68+
eg.schedule_event(curtsies.events.SigIntEvent(), 5)
69+
eg.schedule_event('h', 6)
70+
cb = combined_events(eg, paste_threshold=3)
71+
self.assertEqual(next(cb), 'a')
72+
self.assertEqual(cb.send(0), None)
73+
self.assertEqual(next(cb), 'b')
74+
self.assertEqual(cb.send(0), None)
75+
eg.tick()
76+
self.assertEqual(cb.send(0), 'c')
77+
self.assertEqual(cb.send(0), None)
78+
eg.tick()
79+
self.assertIsInstance(cb.send(0), curtsies.events.PasteEvent)
80+
self.assertEqual(cb.send(0), None)
81+
self.assertEqual(cb.send(None), 'g')
82+
self.assertEqual(cb.send(0), None)
83+
eg.tick(1)
84+
self.assertIsInstance(cb.send(0), curtsies.events.SigIntEvent)
85+
self.assertEqual(cb.send(0), None)
86+
self.assertEqual(cb.send(None), 'h')
87+
self.assertEqual(cb.send(None), None)
88+
89+
90+
if __name__ == '__main__':
91+
unittest.main()
Collapse file

‎bpython/test/test_curtsies_repl.py‎

Copy file name to clipboardExpand all lines: bpython/test/test_curtsies_repl.py
+26Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from bpython.test import (FixLanguageTestCase as TestCase, MagicIterMock, mock,
2121
builtin_target, unittest)
2222

23+
from curtsies import events
24+
2325
if py3:
2426
from importlib import invalidate_caches
2527
else:
@@ -422,5 +424,29 @@ def test_startup_event_latin1(self):
422424
self.assertIn('a', self.repl.interp.locals)
423425

424426

427+
class TestCurtsiesPasteEvents(TestCase):
428+
429+
def setUp(self):
430+
self.repl = create_repl()
431+
432+
def test_control_events_in_small_paste(self):
433+
self.assertGreaterEqual(curtsiesrepl.MAX_EVENTS_POSSIBLY_NOT_PASTE, 6,
434+
'test assumes UI lag could cause 6 events')
435+
p = events.PasteEvent()
436+
p.events = ['a', 'b', 'c', 'd', '<Ctrl-a>', 'e']
437+
self.repl.process_event(p)
438+
self.assertEqual(self.repl.current_line, 'eabcd')
439+
440+
441+
def test_control_events_in_large_paste(self):
442+
"""Large paste events should ignore control characters"""
443+
p = events.PasteEvent()
444+
p.events = (['a', '<Ctrl-a>'] +
445+
['e'] * curtsiesrepl.MAX_EVENTS_POSSIBLY_NOT_PASTE)
446+
self.repl.process_event(p)
447+
self.assertEqual(self.repl.current_line,
448+
'a' + 'e'*curtsiesrepl.MAX_EVENTS_POSSIBLY_NOT_PASTE)
449+
450+
425451
if __name__ == '__main__':
426452
unittest.main()

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.