Skip to content

Commit f9c5e7d

Browse files
committed
Move History to seperate file and improve error handling (fixes bpython#434)
A new method append_reload_and_write is added that handles read and write failures better. Signed-off-by: Sebastian Ramacher <sebastian+dev@ramacher.at>
1 parent 1509a54 commit f9c5e7d

File tree

2 files changed

+236
-155
lines changed

2 files changed

+236
-155
lines changed

bpython/history.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
# The MIT License
2+
#
3+
# Copyright (c) 2009-2015 the bpython authors.
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in
13+
# all copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
# THE SOFTWARE.
22+
23+
24+
import codecs
25+
import os
26+
27+
from bpython.translations import _
28+
29+
30+
class History(object):
31+
"""Stores readline-style history and current place in it"""
32+
33+
def __init__(self, entries=None, duplicates=True, hist_size=100):
34+
if entries is None:
35+
self.entries = ['']
36+
else:
37+
self.entries = list(entries)
38+
self.index = 0 # how many lines back in history is currently selected
39+
# where 0 is the saved typed line, 1 the prev entered line
40+
self.saved_line = '' # what was on the prompt before using history
41+
self.duplicates = duplicates
42+
self.hist_size = hist_size
43+
44+
45+
def append(self, line):
46+
self.append_to(self.entries, line)
47+
48+
49+
def append_to(self, entries, line):
50+
line = line.rstrip('\n')
51+
if line:
52+
if not self.duplicates:
53+
# remove duplicates
54+
try:
55+
while True:
56+
entries.remove(line)
57+
except ValueError:
58+
pass
59+
entries.append(line)
60+
61+
62+
def first(self):
63+
"""Move back to the beginning of the history."""
64+
if not self.is_at_end:
65+
self.index = len(self.entries)
66+
return self.entries[-self.index]
67+
68+
69+
def back(self, start=True, search=False, target=None, include_current=False):
70+
"""Move one step back in the history."""
71+
if target is None:
72+
target = self.saved_line
73+
if not self.is_at_end:
74+
if search:
75+
self.index += self.find_partial_match_backward(target, include_current)
76+
elif start:
77+
self.index += self.find_match_backward(target, include_current)
78+
else:
79+
self.index += 1
80+
return self.entry
81+
82+
83+
@property
84+
def entry(self):
85+
"""The current entry, which may be the saved line"""
86+
return self.entries[-self.index] if self.index else self.saved_line
87+
88+
89+
@property
90+
def entries_by_index(self):
91+
return list(reversed(self.entries + [self.saved_line]))
92+
93+
94+
def find_match_backward(self, search_term, include_current=False):
95+
for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
96+
if val.startswith(search_term):
97+
return idx + (0 if include_current else 1)
98+
return 0
99+
100+
101+
def find_partial_match_backward(self, search_term, include_current=False):
102+
for idx, val in enumerate(self.entries_by_index[self.index + (0 if include_current else 1):]):
103+
if search_term in val:
104+
return idx + (0 if include_current else 1)
105+
return 0
106+
107+
108+
def forward(self, start=True, search=False, target=None, include_current=False):
109+
"""Move one step forward in the history."""
110+
if target is None:
111+
target = self.saved_line
112+
if self.index > 1:
113+
if search:
114+
self.index -= self.find_partial_match_forward(target, include_current)
115+
elif start:
116+
self.index -= self.find_match_forward(target, include_current)
117+
else:
118+
self.index -= 1
119+
return self.entry
120+
else:
121+
self.index = 0
122+
return self.saved_line
123+
124+
125+
def find_match_forward(self, search_term, include_current=False):
126+
#TODO these are no longer efficient, because we realize the whole list. Does this matter?
127+
for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
128+
if val.startswith(search_term):
129+
return idx + (0 if include_current else 1)
130+
return self.index
131+
132+
133+
def find_partial_match_forward(self, search_term, include_current=False):
134+
for idx, val in enumerate(reversed(self.entries_by_index[:max(0, self.index - (1 if include_current else 0))])):
135+
if search_term in val:
136+
return idx + (0 if include_current else 1)
137+
return self.index
138+
139+
140+
def last(self):
141+
"""Move forward to the end of the history."""
142+
if not self.is_at_start:
143+
self.index = 0
144+
return self.entries[0]
145+
146+
147+
@property
148+
def is_at_end(self):
149+
return self.index >= len(self.entries) or self.index == -1
150+
151+
152+
@property
153+
def is_at_start(self):
154+
return self.index == 0
155+
156+
157+
def enter(self, line):
158+
if self.index == 0:
159+
self.saved_line = line
160+
161+
162+
@classmethod
163+
def from_filename(cls, filename):
164+
history = cls()
165+
history.load(filename)
166+
return history
167+
168+
169+
def reset(self):
170+
self.index = 0
171+
self.saved_line = ''
172+
173+
174+
def load(self, filename, encoding):
175+
with codecs.open(filename, 'r', encoding, 'ignore') as hfile:
176+
self.entries = self.load_from(hfile)
177+
178+
179+
def load_from(self, fd):
180+
entries = []
181+
for line in fd:
182+
self.append_to(entries, line)
183+
return entries
184+
185+
186+
def save(self, filename, encoding, lines=0):
187+
with codecs.open(filename, 'w', encoding, 'ignore') as hfile:
188+
self.save_to(hfile, self.entries, lines)
189+
190+
191+
def save_to(self, fd, entries=None, lines=0):
192+
if entries is None:
193+
entries = self.entries
194+
for line in entries[-lines:]:
195+
fd.write(line)
196+
fd.write('\n')
197+
198+
199+
def append_reload_and_write(self, s, filename, encoding):
200+
if not self.hist_size:
201+
return self.append(s)
202+
203+
try:
204+
with codecs.open(filename, 'rw+', encoding, 'ignore') as hfile:
205+
# read entries
206+
hfile.seek(0, os.SEEK_SET)
207+
entries = self.load_from(hfile)
208+
self.append_to(entries, s)
209+
210+
# write new entries
211+
hfile.seek(0, os.SEEK_SET)
212+
hfile.truncate()
213+
self.save_to(hfile, entries, self.hist_size)
214+
215+
self.entries = entries
216+
except EnvironmentError as err:
217+
raise RuntimeError(_('Error occurded while writing to file %s (%s)')
218+
% (filename, err.strerror))
219+
else:
220+
if len(self.entries) == 0:
221+
# Make sure that entries contains at least one element. If the
222+
# file and s are empty, this can occur.
223+
self.entries = ['']

0 commit comments

Comments
 (0)