##// END OF EJS Templates
Initial conversion of history to SQLite database.
Thomas Kluyver -
Show More
@@ -13,13 +13,8 b''
13 13 from __future__ import print_function
14 14
15 15 # Stdlib imports
16 import atexit
17 import fnmatch
18 import json
19 16 import os
20 import sys
21 import threading
22 import time
17 import sqlite3
23 18
24 19 # Our own packages
25 20 import IPython.utils.io
@@ -50,14 +45,10 b' class HistoryManager(object):'
50 45 output_hist = None
51 46 # String with path to the history file
52 47 hist_file = None
53 # PickleShareDB instance holding the raw data for the shadow history
54 shadow_db = None
55 # ShadowHist instance with the actual shadow history
56 shadow_hist = 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
48 # The SQLite database
49 db = None
50 # The number of the current session in the history database
51 session_number = None
61 52
62 53 # Private interface
63 54 # Variables used to store the three last inputs from the user. On each new
@@ -83,12 +74,13 b' class HistoryManager(object):'
83 74 # We need a pointer back to the shell for various tasks.
84 75 self.shell = shell
85 76
86 # List of input with multi-line handling.
87 self.input_hist_parsed = []
77 # List of input with multi-line handling. One blank entry so indexing
78 # starts from 1.
79 self.input_hist_parsed = [""]
88 80 # This one will hold the 'raw' input history, without any
89 81 # pre-processing. This will allow users to retrieve the input just as
90 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 85 # list of visited directories
94 86 try:
@@ -104,99 +96,100 b' class HistoryManager(object):'
104 96 histfname = 'history-%s' % shell.profile
105 97 else:
106 98 histfname = 'history'
107 self.hist_file = os.path.join(shell.ipython_dir, histfname + '.json')
108
109 # Objects related to shadow history management
110 self._init_shadow_hist()
99 self.hist_file = os.path.join(shell.ipython_dir, histfname + '.sqlite')
111 100
112 101 self._i00, self._i, self._ii, self._iii = '','','',''
113 102
114 103 self._exit_commands = set(['Quit', 'quit', 'Exit', 'exit', '%Quit',
115 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:
120 self.reload_history()
121 self.session_offset = len(self.input_hist_raw) -1
108 def init_db(self):
109 self.db = sqlite3.connect(self.hist_file)
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.
124 self.autosave_flag = threading.Event()
125 self.autosave_timer = HistorySaveThread(self.autosave_flag, 60)
126 self.autosave_timer.start()
127 # Register the autosave handler to be triggered as a post execute
128 # callback.
129 self.shell.register_post_execute(self.autosave_if_due)
125 #Increment by one for next session.
126 self.db.execute("""UPDATE singletons SET value=? WHERE
127 name='session_number'""", (self.session_number+1,))
128 self.db.commit()
129
130 def get_db_history(self, session, start=1, stop=None, raw=True):
131 """Retrieve input history from the database by session.
130 132
131
132 def _init_shadow_hist(self):
133 try:
134 self.shadow_db = PickleShareDB(os.path.join(
135 self.shell.ipython_dir, 'db'))
136 except UnicodeDecodeError:
137 print("Your ipython_dir can't be decoded to unicode!")
138 print("Please set HOME environment variable to something that")
139 print(r"only has ASCII characters, e.g. c:\home")
140 print("Now it is", self.ipython_dir)
141 sys.exit()
142 self.shadow_hist = ShadowHist(self.shadow_db, self.shell)
133 Parameters
134 ----------
135 session : int
136 Session number to retrieve. If negative, counts back from current
137 session (so -1 is previous session).
138 start : int
139 First line to retrieve.
140 stop : int
141 Last line to retrieve. If None, retrieve to the end of the session.
142 raw : bool
143 If True, return raw input
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):
145 """Populate the readline history from the raw history.
146
147 We only store one copy of the raw history, which is persisted to a json
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
153 if stop:
154 cur = self.db.execute("SELECT " + toget + """ FROM history WHERE
155 session==? AND line BETWEEN ? and ?""",
156 (session, start, stop))
155 157 else:
156 for h in self.input_hist_raw:
157 if not h.isspace():
158 for line in h.splitlines():
159 self.shell.readline.add_history(line)
160
161 def save_history(self):
162 """Save input history to a file (via readline library)."""
163 hist = dict(raw=self.input_hist_raw, #[-self.shell.history_length:],
164 parsed=self.input_hist_parsed) #[-self.shell.history_length:])
165 with open(self.hist_file,'wt') as hfile:
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()
158 cur = self.db.execute("SELECT " + toget + """ FROM history WHERE
159 session==? AND line>=?""", (session, start))
160 return (x[0] for x in cur)
161
162 def tail_db_history(self, n=10, raw=True):
163 """Get the last n lines from the history database."""
164 toget = 'source_raw' if raw else 'source'
165 cur = self.db.execute("SELECT " + toget + """ FROM history ORDER BY
166 session DESC, line DESC LIMIT ?""", (n,))
167 return (x[0] for x in reversed(cur.fetchall()))
175 168
176 def reload_history(self):
177 """Reload the input history from disk file."""
178
179 with open(self.hist_file,'rt') as hfile:
180 try:
181 hist = json.load(hfile)
182 except ValueError: # Ignore it if JSON is corrupt.
183 return
184 self.input_hist_parsed = hist['parsed']
185 self.input_hist_raw = hist['raw']
186 if self.shell.has_readline:
187 self.populate_readline_history()
169 def globsearch_db(self, pattern="*"):
170 """Search the database using unix glob-style matching (wildcards * and
171 ?, escape using \).
172
173 Returns
174 -------
175 An iterator over tuples: (session, line_number, command)
176 """
177 return self.db.execute("""SELECT session, line, source_raw FROM history
178 WHERE source_raw GLOB ?""", (pattern,))
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 181 """Get the history list.
191 182
192 183 Get the input and output history.
193 184
194 185 Parameters
195 186 ----------
196 index : n or (n1, n2) or None
197 If n, then the last n entries. If a tuple, then all in
198 range(n1, n2). If None, then all entries. Raises IndexError if
199 the format of index is incorrect.
187 start : int
188 From (prompt number in the current session). Negative numbers count
189 back from the end.
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 193 raw : bool
201 194 If True, return the raw input.
202 195 output : bool
@@ -217,29 +210,21 b' class HistoryManager(object):'
217 210 input_hist = self.input_hist_parsed
218 211 if output:
219 212 output_hist = self.output_hist
220
221 if this_session:
222 offset = self.session_offset
223 else:
224 offset = -1
225 213
226 214 n = len(input_hist)
227 if index is None:
228 start=offset+1; stop=n
229 elif isinstance(index, int):
230 start=n-index; stop=n
231 elif len(index) == 2:
232 start = index[0] + offset
233 stop = index[1] + offset
234 else:
235 raise IndexError('Not a valid index for the input history: %r'
236 % index)
215 if start < 0:
216 start += n
217 if not stop:
218 stop = n
219 elif stop < 0:
220 stop += n
221
237 222 hist = {}
238 223 for i in range(start, stop):
239 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 226 else:
242 hist[i-offset] = input_hist[i]
227 hist[i] = input_hist[i]
243 228 return hist
244 229
245 230 def store_inputs(self, source, source_raw=None):
@@ -264,7 +249,10 b' class HistoryManager(object):'
264 249
265 250 self.input_hist_parsed.append(source.rstrip())
266 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 257 # update the auto _i variables
270 258 self._iii = self._ii
@@ -282,8 +270,12 b' class HistoryManager(object):'
282 270
283 271 def sync_inputs(self):
284 272 """Ensure raw and translated histories have same length."""
285 if len(self.input_hist_parsed) != len (self.input_hist_raw):
286 self.input_hist_raw[:] = self.input_hist_parsed
273 lr = len(self.input_hist_raw)
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 280 def reset(self):
289 281 """Clear all histories managed by this object."""
@@ -292,41 +284,6 b' class HistoryManager(object):'
292 284 self.output_hist.clear()
293 285 # The directory history can't be completely empty
294 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 288 @testdec.skip_doctest
332 289 def magic_history(self, parameter_s = ''):
@@ -364,9 +321,8 b" def magic_history(self, parameter_s = ''):"
364 321 'get_ipython().magic("%cd /")' instead of '%cd /'.
365 322
366 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).
368 Use '%hist -g' to show full shadow history (may be very long).
369 In shadow history, every index nuwber starts with 0.
324 This includes the saved history (almost all commands ever written).
325 Use '%hist -g' to show full saved history (may be very long).
370 326
371 327 -f FILENAME: instead of printing the output to the screen, redirect it to
372 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 371 default_length = 40
416 372 pattern = None
417 373 if 'g' in opts:
418 index = None
374 start = 1; stop = None
419 375 parts = parameter_s.split(None, 1)
420 376 if len(parts) == 1:
421 377 parts += '*'
422 378 head, pattern = parts
423 379 pattern = "*" + pattern + "*"
424 380 elif len(args) == 0:
425 index = None
381 start = 1; stop = None
426 382 elif len(args) == 1:
427 index = int(args[0])
383 start = -int(args[0]); stop=None
428 384 elif len(args) == 2:
429 index = map(int, args)
385 start = int(args[0]); stop = int(args[1])
430 386 else:
431 387 warn('%hist takes 0, 1 or 2 arguments separated by spaces.')
432 388 print(self.magic_hist.__doc__, file=IPython.utils.io.Term.cout)
433 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 393 width = len(str(max(hist.iterkeys())))
438 394 line_sep = ['','\n']
439 395
440 396 found = False
441 397 if pattern is not None:
442 sh = history_manager.shadow_hist.all()
443 for idx, s in sh:
444 if fnmatch.fnmatch(s, pattern):
445 print("0%d: %s" %(idx, s.expandtabs(4)), file=outfile)
446 found = True
398 for session, line, s in history_manager.globsearch_db(pattern):
399 print("%d#%d: %s" %(session, line, s.expandtabs(4)), file=outfile)
400 found = True
447 401
448 402 if found:
449 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 405 file=outfile)
452 406 print("=== start of normal history ===", file=outfile)
453 407
@@ -545,50 +499,6 b' def rep_f(self, arg):'
545 499 self.run_cell(lines)
546 500 except ValueError:
547 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 504 def init_ipython(ip):
@@ -56,11 +56,11 b' from IPython.core.prefilter import PrefilterManager, ESC_MAGIC'
56 56 from IPython.external.Itpl import ItplNS
57 57 from IPython.utils import PyColorize
58 58 from IPython.utils import io
59 from IPython.utils import pickleshare
60 59 from IPython.utils.doctestreload import doctest_reload
61 60 from IPython.utils.io import ask_yes_no, rprint
62 61 from IPython.utils.ipstruct import Struct
63 62 from IPython.utils.path import get_home_dir, get_ipython_dir, HomeDirError
63 from IPython.utils.pickleshare import PickleShareDB
64 64 from IPython.utils.process import system, getoutput
65 65 from IPython.utils.strdispatch import StrDispatch
66 66 from IPython.utils.syspathcontext import prepended_to_syspath
@@ -250,6 +250,11 b' class InteractiveShell(Configurable, Magic):'
250 250 # is what we want to do.
251 251 self.save_sys_module_state()
252 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 259 self.init_history()
255 260 self.init_encoding()
@@ -300,14 +305,6 b' class InteractiveShell(Configurable, Magic):'
300 305 self.hooks.late_startup_hook()
301 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 308 @classmethod
312 309 def instance(cls, *args, **kwargs):
313 310 """Returns a global InteractiveShell instance."""
@@ -1248,15 +1245,7 b' class InteractiveShell(Configurable, Magic):'
1248 1245
1249 1246 def init_history(self):
1250 1247 """Sets up the command history, and starts regular autosaves."""
1251 self.history_manager = HistoryManager(shell=self, load_history=True)
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()
1248 self.history_manager = HistoryManager(shell=self)
1260 1249
1261 1250 def history_saving_wrapper(self, func):
1262 1251 """ Wrap func for readline history saving
@@ -1277,8 +1266,8 b' class InteractiveShell(Configurable, Magic):'
1277 1266 self.reload_history()
1278 1267 return wrapper
1279 1268
1280 def get_history(self, index=None, raw=False, output=True,this_session=True):
1281 return self.history_manager.get_history(index, raw, output,this_session)
1269 def get_history(self, start=1, stop=None, raw=False, output=True):
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 1550 # otherwise we end up with a monster history after a while:
1562 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 1559 # Configure auto-indent for all platforms
1567 1560 self.set_autoindent(self.autoindent)
@@ -2536,8 +2529,6 b' class InteractiveShell(Configurable, Magic):'
2536 2529 except OSError:
2537 2530 pass
2538 2531
2539 self.save_history()
2540
2541 2532 # Clear all user namespaces to release all references cleanly.
2542 2533 self.reset()
2543 2534
General Comments 0
You need to be logged in to leave comments. Login now