##// END OF EJS Templates
Initial conversion of history to SQLite database.
Thomas Kluyver -
Show More
@@ -13,13 +13,8 b''
13 from __future__ import print_function
13 from __future__ import print_function
14
14
15 # Stdlib imports
15 # Stdlib imports
16 import atexit
17 import fnmatch
18 import json
19 import os
16 import os
20 import sys
17 import sqlite3
21 import threading
22 import time
23
18
24 # Our own packages
19 # Our own packages
25 import IPython.utils.io
20 import IPython.utils.io
@@ -50,14 +45,10 b' class HistoryManager(object):'
50 output_hist = None
45 output_hist = None
51 # String with path to the history file
46 # String with path to the history file
52 hist_file = None
47 hist_file = None
53 # PickleShareDB instance holding the raw data for the shadow history
48 # The SQLite database
54 shadow_db = None
49 db = None
55 # ShadowHist instance with the actual shadow history
50 # The number of the current session in the history database
56 shadow_hist = None
51 session_number = None
57
58 # Offset so the first line of the current session is #1. Can be
59 # updated after loading history from file.
60 session_offset = -1
61
52
62 # Private interface
53 # Private interface
63 # Variables used to store the three last inputs from the user. On each new
54 # Variables used to store the three last inputs from the user. On each new
@@ -83,12 +74,13 b' class HistoryManager(object):'
83 # We need a pointer back to the shell for various tasks.
74 # We need a pointer back to the shell for various tasks.
84 self.shell = shell
75 self.shell = shell
85
76
86 # List of input with multi-line handling.
77 # List of input with multi-line handling. One blank entry so indexing
87 self.input_hist_parsed = []
78 # starts from 1.
79 self.input_hist_parsed = [""]
88 # This one will hold the 'raw' input history, without any
80 # This one will hold the 'raw' input history, without any
89 # pre-processing. This will allow users to retrieve the input just as
81 # pre-processing. This will allow users to retrieve the input just as
90 # it was exactly typed in by the user, with %hist -r.
82 # it was exactly typed in by the user, with %hist -r.
91 self.input_hist_raw = []
83 self.input_hist_raw = [""]
92
84
93 # list of visited directories
85 # list of visited directories
94 try:
86 try:
@@ -104,99 +96,100 b' class HistoryManager(object):'
104 histfname = 'history-%s' % shell.profile
96 histfname = 'history-%s' % shell.profile
105 else:
97 else:
106 histfname = 'history'
98 histfname = 'history'
107 self.hist_file = os.path.join(shell.ipython_dir, histfname + '.json')
99 self.hist_file = os.path.join(shell.ipython_dir, histfname + '.sqlite')
108
109 # Objects related to shadow history management
110 self._init_shadow_hist()
111
100
112 self._i00, self._i, self._ii, self._iii = '','','',''
101 self._i00, self._i, self._ii, self._iii = '','','',''
113
102
114 self._exit_commands = set(['Quit', 'quit', 'Exit', 'exit', '%Quit',
103 self._exit_commands = set(['Quit', 'quit', 'Exit', 'exit', '%Quit',
115 '%quit', '%Exit', '%exit'])
104 '%quit', '%Exit', '%exit'])
116
105
117 # Object is fully initialized, we can now call methods on it.
106 self.init_db()
118
107
119 if load_history:
108 def init_db(self):
120 self.reload_history()
109 self.db = sqlite3.connect(self.hist_file)
121 self.session_offset = len(self.input_hist_raw) -1
110 self.db.execute("""CREATE TABLE IF NOT EXISTS history (session integer,
111 line integer, source text, source_raw text,
112 PRIMARY KEY (session, line))""")
113 cur = self.db.execute("""SELECT name FROM sqlite_master WHERE
114 type='table' AND name='singletons'""")
115 if not cur.fetchone():
116 self.db.execute("""CREATE TABLE singletons
117 (name text PRIMARY KEY, value)""")
118 self.db.execute("""INSERT INTO singletons VALUES
119 ('session_number', 1)""")
120 self.db.commit()
121 cur = self.db.execute("""SELECT value FROM singletons WHERE
122 name='session_number'""")
123 self.session_number = cur.fetchone()[0]
122
124
123 # Create and start the autosaver.
125 #Increment by one for next session.
124 self.autosave_flag = threading.Event()
126 self.db.execute("""UPDATE singletons SET value=? WHERE
125 self.autosave_timer = HistorySaveThread(self.autosave_flag, 60)
127 name='session_number'""", (self.session_number+1,))
126 self.autosave_timer.start()
128 self.db.commit()
127 # Register the autosave handler to be triggered as a post execute
129
128 # callback.
130 def get_db_history(self, session, start=1, stop=None, raw=True):
129 self.shell.register_post_execute(self.autosave_if_due)
131 """Retrieve input history from the database by session.
130
132
131
133 Parameters
132 def _init_shadow_hist(self):
134 ----------
133 try:
135 session : int
134 self.shadow_db = PickleShareDB(os.path.join(
136 Session number to retrieve. If negative, counts back from current
135 self.shell.ipython_dir, 'db'))
137 session (so -1 is previous session).
136 except UnicodeDecodeError:
138 start : int
137 print("Your ipython_dir can't be decoded to unicode!")
139 First line to retrieve.
138 print("Please set HOME environment variable to something that")
140 stop : int
139 print(r"only has ASCII characters, e.g. c:\home")
141 Last line to retrieve. If None, retrieve to the end of the session.
140 print("Now it is", self.ipython_dir)
142 raw : bool
141 sys.exit()
143 If True, return raw input
142 self.shadow_hist = ShadowHist(self.shadow_db, self.shell)
144
145 Returns
146 -------
147 An iterator over the desired lines.
148 """
149 toget = 'source_raw' if raw else 'source'
150 if session < 0:
151 session += self.session_number
143
152
144 def populate_readline_history(self):
153 if stop:
145 """Populate the readline history from the raw history.
154 cur = self.db.execute("SELECT " + toget + """ FROM history WHERE
146
155 session==? AND line BETWEEN ? and ?""",
147 We only store one copy of the raw history, which is persisted to a json
156 (session, start, stop))
148 file on disk. The readline history is repopulated from the contents of
149 this file."""
150
151 try:
152 self.shell.readline.clear_history()
153 except AttributeError:
154 pass
155 else:
157 else:
156 for h in self.input_hist_raw:
158 cur = self.db.execute("SELECT " + toget + """ FROM history WHERE
157 if not h.isspace():
159 session==? AND line>=?""", (session, start))
158 for line in h.splitlines():
160 return (x[0] for x in cur)
159 self.shell.readline.add_history(line)
161
160
162 def tail_db_history(self, n=10, raw=True):
161 def save_history(self):
163 """Get the last n lines from the history database."""
162 """Save input history to a file (via readline library)."""
164 toget = 'source_raw' if raw else 'source'
163 hist = dict(raw=self.input_hist_raw, #[-self.shell.history_length:],
165 cur = self.db.execute("SELECT " + toget + """ FROM history ORDER BY
164 parsed=self.input_hist_parsed) #[-self.shell.history_length:])
166 session DESC, line DESC LIMIT ?""", (n,))
165 with open(self.hist_file,'wt') as hfile:
167 return (x[0] for x in reversed(cur.fetchall()))
166 json.dump(hist, hfile,
167 sort_keys=True, indent=4)
168
169 def autosave_if_due(self):
170 """Check if the autosave event is set; if so, save history. We do it
171 this way so that the save takes place in the main thread."""
172 if self.autosave_flag.is_set():
173 self.save_history()
174 self.autosave_flag.clear()
175
168
176 def reload_history(self):
169 def globsearch_db(self, pattern="*"):
177 """Reload the input history from disk file."""
170 """Search the database using unix glob-style matching (wildcards * and
178
171 ?, escape using \).
179 with open(self.hist_file,'rt') as hfile:
172
180 try:
173 Returns
181 hist = json.load(hfile)
174 -------
182 except ValueError: # Ignore it if JSON is corrupt.
175 An iterator over tuples: (session, line_number, command)
183 return
176 """
184 self.input_hist_parsed = hist['parsed']
177 return self.db.execute("""SELECT session, line, source_raw FROM history
185 self.input_hist_raw = hist['raw']
178 WHERE source_raw GLOB ?""", (pattern,))
186 if self.shell.has_readline:
187 self.populate_readline_history()
188
179
189 def get_history(self, index=None, raw=False, output=True,this_session=True):
180 def get_history(self, start=1, stop=None, raw=False, output=True):
190 """Get the history list.
181 """Get the history list.
191
182
192 Get the input and output history.
183 Get the input and output history.
193
184
194 Parameters
185 Parameters
195 ----------
186 ----------
196 index : n or (n1, n2) or None
187 start : int
197 If n, then the last n entries. If a tuple, then all in
188 From (prompt number in the current session). Negative numbers count
198 range(n1, n2). If None, then all entries. Raises IndexError if
189 back from the end.
199 the format of index is incorrect.
190 stop : int
191 To (prompt number in the current session, exclusive). Negative
192 numbers count back from the end, and None goes to the end.
200 raw : bool
193 raw : bool
201 If True, return the raw input.
194 If True, return the raw input.
202 output : bool
195 output : bool
@@ -217,29 +210,21 b' class HistoryManager(object):'
217 input_hist = self.input_hist_parsed
210 input_hist = self.input_hist_parsed
218 if output:
211 if output:
219 output_hist = self.output_hist
212 output_hist = self.output_hist
220
221 if this_session:
222 offset = self.session_offset
223 else:
224 offset = -1
225
213
226 n = len(input_hist)
214 n = len(input_hist)
227 if index is None:
215 if start < 0:
228 start=offset+1; stop=n
216 start += n
229 elif isinstance(index, int):
217 if not stop:
230 start=n-index; stop=n
218 stop = n
231 elif len(index) == 2:
219 elif stop < 0:
232 start = index[0] + offset
220 stop += n
233 stop = index[1] + offset
221
234 else:
235 raise IndexError('Not a valid index for the input history: %r'
236 % index)
237 hist = {}
222 hist = {}
238 for i in range(start, stop):
223 for i in range(start, stop):
239 if output:
224 if output:
240 hist[i-offset] = (input_hist[i], output_hist.get(i-offset))
225 hist[i] = (input_hist[i], output_hist.get(i))
241 else:
226 else:
242 hist[i-offset] = input_hist[i]
227 hist[i] = input_hist[i]
243 return hist
228 return hist
244
229
245 def store_inputs(self, source, source_raw=None):
230 def store_inputs(self, source, source_raw=None):
@@ -264,7 +249,10 b' class HistoryManager(object):'
264
249
265 self.input_hist_parsed.append(source.rstrip())
250 self.input_hist_parsed.append(source.rstrip())
266 self.input_hist_raw.append(source_raw.rstrip())
251 self.input_hist_raw.append(source_raw.rstrip())
267 self.shadow_hist.add(source)
252 with self.db:
253 self.db.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
254 (self.session_number, self.shell.execution_count,
255 source, source_raw))
268
256
269 # update the auto _i variables
257 # update the auto _i variables
270 self._iii = self._ii
258 self._iii = self._ii
@@ -282,8 +270,12 b' class HistoryManager(object):'
282
270
283 def sync_inputs(self):
271 def sync_inputs(self):
284 """Ensure raw and translated histories have same length."""
272 """Ensure raw and translated histories have same length."""
285 if len(self.input_hist_parsed) != len (self.input_hist_raw):
273 lr = len(self.input_hist_raw)
286 self.input_hist_raw[:] = self.input_hist_parsed
274 lp = len(self.input_hist_parsed)
275 if lp < lr:
276 self.input_hist_raw[:lr-lp] = []
277 elif lr < lp:
278 self.input_hist_parsed[:lp-lr] = []
287
279
288 def reset(self):
280 def reset(self):
289 """Clear all histories managed by this object."""
281 """Clear all histories managed by this object."""
@@ -292,41 +284,6 b' class HistoryManager(object):'
292 self.output_hist.clear()
284 self.output_hist.clear()
293 # The directory history can't be completely empty
285 # The directory history can't be completely empty
294 self.dir_hist[:] = [os.getcwd()]
286 self.dir_hist[:] = [os.getcwd()]
295 # Reset session offset to -1, so next command counts as #1
296 self.session_offset = -1
297
298 class HistorySaveThread(threading.Thread):
299 """This thread makes IPython save history periodically.
300
301 Without this class, IPython would only save the history on a clean exit.
302 This saves the history periodically (the current default is once per
303 minute), so that it is not lost in the event of a crash.
304
305 The implementation sets an event to indicate that history should be saved.
306 The actual save is carried out after executing a user command, to avoid
307 thread issues.
308 """
309 daemon = True
310
311 def __init__(self, autosave_flag, time_interval=60):
312 threading.Thread.__init__(self)
313 self.time_interval = time_interval
314 self.autosave_flag = autosave_flag
315 self.exit_now = threading.Event()
316 # Ensure the thread is stopped tidily when exiting normally
317 atexit.register(self.stop)
318
319 def run(self):
320 while True:
321 self.exit_now.wait(self.time_interval)
322 if self.exit_now.is_set():
323 break
324 self.autosave_flag.set()
325
326 def stop(self):
327 """Safely and quickly stop the autosave timer thread."""
328 self.exit_now.set()
329 self.join()
330
287
331 @testdec.skip_doctest
288 @testdec.skip_doctest
332 def magic_history(self, parameter_s = ''):
289 def magic_history(self, parameter_s = ''):
@@ -364,9 +321,8 b" def magic_history(self, parameter_s = ''):"
364 'get_ipython().magic("%cd /")' instead of '%cd /'.
321 'get_ipython().magic("%cd /")' instead of '%cd /'.
365
322
366 -g: treat the arg as a pattern to grep for in (full) history.
323 -g: treat the arg as a pattern to grep for in (full) history.
367 This includes the "shadow history" (almost all commands ever written).
324 This includes the saved history (almost all commands ever written).
368 Use '%hist -g' to show full shadow history (may be very long).
325 Use '%hist -g' to show full saved history (may be very long).
369 In shadow history, every index nuwber starts with 0.
370
326
371 -f FILENAME: instead of printing the output to the screen, redirect it to
327 -f FILENAME: instead of printing the output to the screen, redirect it to
372 the given file. The file is always overwritten, though IPython asks for
328 the given file. The file is always overwritten, though IPython asks for
@@ -415,39 +371,37 b" def magic_history(self, parameter_s = ''):"
415 default_length = 40
371 default_length = 40
416 pattern = None
372 pattern = None
417 if 'g' in opts:
373 if 'g' in opts:
418 index = None
374 start = 1; stop = None
419 parts = parameter_s.split(None, 1)
375 parts = parameter_s.split(None, 1)
420 if len(parts) == 1:
376 if len(parts) == 1:
421 parts += '*'
377 parts += '*'
422 head, pattern = parts
378 head, pattern = parts
423 pattern = "*" + pattern + "*"
379 pattern = "*" + pattern + "*"
424 elif len(args) == 0:
380 elif len(args) == 0:
425 index = None
381 start = 1; stop = None
426 elif len(args) == 1:
382 elif len(args) == 1:
427 index = int(args[0])
383 start = -int(args[0]); stop=None
428 elif len(args) == 2:
384 elif len(args) == 2:
429 index = map(int, args)
385 start = int(args[0]); stop = int(args[1])
430 else:
386 else:
431 warn('%hist takes 0, 1 or 2 arguments separated by spaces.')
387 warn('%hist takes 0, 1 or 2 arguments separated by spaces.')
432 print(self.magic_hist.__doc__, file=IPython.utils.io.Term.cout)
388 print(self.magic_hist.__doc__, file=IPython.utils.io.Term.cout)
433 return
389 return
434
390
435 hist = history_manager.get_history(index, raw, print_outputs)
391 hist = history_manager.get_history(start, stop, raw, print_outputs)
436
392
437 width = len(str(max(hist.iterkeys())))
393 width = len(str(max(hist.iterkeys())))
438 line_sep = ['','\n']
394 line_sep = ['','\n']
439
395
440 found = False
396 found = False
441 if pattern is not None:
397 if pattern is not None:
442 sh = history_manager.shadow_hist.all()
398 for session, line, s in history_manager.globsearch_db(pattern):
443 for idx, s in sh:
399 print("%d#%d: %s" %(session, line, s.expandtabs(4)), file=outfile)
444 if fnmatch.fnmatch(s, pattern):
400 found = True
445 print("0%d: %s" %(idx, s.expandtabs(4)), file=outfile)
446 found = True
447
401
448 if found:
402 if found:
449 print("===", file=outfile)
403 print("===", file=outfile)
450 print("shadow history ends, fetch by %rep <number> (must start with 0)",
404 print("shadow history ends, fetch by %rep session#line",
451 file=outfile)
405 file=outfile)
452 print("=== start of normal history ===", file=outfile)
406 print("=== start of normal history ===", file=outfile)
453
407
@@ -545,50 +499,6 b' def rep_f(self, arg):'
545 self.run_cell(lines)
499 self.run_cell(lines)
546 except ValueError:
500 except ValueError:
547 print("Not found in recent history:", args)
501 print("Not found in recent history:", args)
548
549
550 _sentinel = object()
551
552 class ShadowHist(object):
553 def __init__(self, db, shell):
554 # cmd => idx mapping
555 self.curidx = 0
556 self.db = db
557 self.disabled = False
558 self.shell = shell
559
560 def inc_idx(self):
561 idx = self.db.get('shadowhist_idx', 1)
562 self.db['shadowhist_idx'] = idx + 1
563 return idx
564
565 def add(self, ent):
566 if self.disabled:
567 return
568 try:
569 old = self.db.hget('shadowhist', ent, _sentinel)
570 if old is not _sentinel:
571 return
572 newidx = self.inc_idx()
573 #print("new", newidx) # dbg
574 self.db.hset('shadowhist',ent, newidx)
575 except:
576 self.shell.showtraceback()
577 print("WARNING: disabling shadow history")
578 self.disabled = True
579
580 def all(self):
581 d = self.db.hdict('shadowhist')
582 items = [(i,s) for (s,i) in d.iteritems()]
583 items.sort()
584 return items
585
586 def get(self, idx):
587 all = self.all()
588
589 for k, v in all:
590 if k == idx:
591 return v
592
502
593
503
594 def init_ipython(ip):
504 def init_ipython(ip):
@@ -56,11 +56,11 b' from IPython.core.prefilter import PrefilterManager, ESC_MAGIC'
56 from IPython.external.Itpl import ItplNS
56 from IPython.external.Itpl import ItplNS
57 from IPython.utils import PyColorize
57 from IPython.utils import PyColorize
58 from IPython.utils import io
58 from IPython.utils import io
59 from IPython.utils import pickleshare
60 from IPython.utils.doctestreload import doctest_reload
59 from IPython.utils.doctestreload import doctest_reload
61 from IPython.utils.io import ask_yes_no, rprint
60 from IPython.utils.io import ask_yes_no, rprint
62 from IPython.utils.ipstruct import Struct
61 from IPython.utils.ipstruct import Struct
63 from IPython.utils.path import get_home_dir, get_ipython_dir, HomeDirError
62 from IPython.utils.path import get_home_dir, get_ipython_dir, HomeDirError
63 from IPython.utils.pickleshare import PickleShareDB
64 from IPython.utils.process import system, getoutput
64 from IPython.utils.process import system, getoutput
65 from IPython.utils.strdispatch import StrDispatch
65 from IPython.utils.strdispatch import StrDispatch
66 from IPython.utils.syspathcontext import prepended_to_syspath
66 from IPython.utils.syspathcontext import prepended_to_syspath
@@ -250,6 +250,11 b' class InteractiveShell(Configurable, Magic):'
250 # is what we want to do.
250 # is what we want to do.
251 self.save_sys_module_state()
251 self.save_sys_module_state()
252 self.init_sys_modules()
252 self.init_sys_modules()
253
254 # While we're trying to have each part of the code directly access what
255 # it needs without keeping redundant references to objects, we have too
256 # much legacy code that expects ip.db to exist.
257 self.db = PickleShareDB(os.path.join(self.ipython_dir, 'db'))
253
258
254 self.init_history()
259 self.init_history()
255 self.init_encoding()
260 self.init_encoding()
@@ -300,14 +305,6 b' class InteractiveShell(Configurable, Magic):'
300 self.hooks.late_startup_hook()
305 self.hooks.late_startup_hook()
301 atexit.register(self.atexit_operations)
306 atexit.register(self.atexit_operations)
302
307
303 # While we're trying to have each part of the code directly access what it
304 # needs without keeping redundant references to objects, we have too much
305 # legacy code that expects ip.db to exist, so let's make it a property that
306 # retrieves the underlying object from our new history manager.
307 @property
308 def db(self):
309 return self.history_manager.shadow_db
310
311 @classmethod
308 @classmethod
312 def instance(cls, *args, **kwargs):
309 def instance(cls, *args, **kwargs):
313 """Returns a global InteractiveShell instance."""
310 """Returns a global InteractiveShell instance."""
@@ -1248,15 +1245,7 b' class InteractiveShell(Configurable, Magic):'
1248
1245
1249 def init_history(self):
1246 def init_history(self):
1250 """Sets up the command history, and starts regular autosaves."""
1247 """Sets up the command history, and starts regular autosaves."""
1251 self.history_manager = HistoryManager(shell=self, load_history=True)
1248 self.history_manager = HistoryManager(shell=self)
1252
1253 def save_history(self):
1254 """Save input history to a file (via readline library)."""
1255 self.history_manager.save_history()
1256
1257 def reload_history(self):
1258 """Reload the input history from disk file."""
1259 self.history_manager.reload_history()
1260
1249
1261 def history_saving_wrapper(self, func):
1250 def history_saving_wrapper(self, func):
1262 """ Wrap func for readline history saving
1251 """ Wrap func for readline history saving
@@ -1277,8 +1266,8 b' class InteractiveShell(Configurable, Magic):'
1277 self.reload_history()
1266 self.reload_history()
1278 return wrapper
1267 return wrapper
1279
1268
1280 def get_history(self, index=None, raw=False, output=True,this_session=True):
1269 def get_history(self, start=1, stop=None, raw=False, output=True):
1281 return self.history_manager.get_history(index, raw, output,this_session)
1270 return self.history_manager.get_history(start, stop, raw, output)
1282
1271
1283
1272
1284 #-------------------------------------------------------------------------
1273 #-------------------------------------------------------------------------
@@ -1561,7 +1550,11 b' class InteractiveShell(Configurable, Magic):'
1561 # otherwise we end up with a monster history after a while:
1550 # otherwise we end up with a monster history after a while:
1562 readline.set_history_length(self.history_length)
1551 readline.set_history_length(self.history_length)
1563
1552
1564 self.history_manager.populate_readline_history()
1553 # Load the last 1000 lines from history
1554 for cell in self.history_manager.tail_db_history(1000):
1555 if cell.strip(): # Ignore blank lines
1556 for line in cell.splitlines():
1557 readline.add_history(line)
1565
1558
1566 # Configure auto-indent for all platforms
1559 # Configure auto-indent for all platforms
1567 self.set_autoindent(self.autoindent)
1560 self.set_autoindent(self.autoindent)
@@ -2536,8 +2529,6 b' class InteractiveShell(Configurable, Magic):'
2536 except OSError:
2529 except OSError:
2537 pass
2530 pass
2538
2531
2539 self.save_history()
2540
2541 # Clear all user namespaces to release all references cleanly.
2532 # Clear all user namespaces to release all references cleanly.
2542 self.reset()
2533 self.reset()
2543
2534
General Comments 0
You need to be logged in to leave comments. Login now