##// END OF EJS Templates
Updating comments and docstrings for history autosave.
Thomas Kluyver -
Show More
@@ -1,576 +1,578 b''
1 1 """ History related magics and functionality """
2 2 #-----------------------------------------------------------------------------
3 3 # Copyright (C) 2010 The IPython Development Team.
4 4 #
5 5 # Distributed under the terms of the BSD License.
6 6 #
7 7 # The full license is in the file COPYING.txt, distributed with this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13 from __future__ import print_function
14 14
15 15 # Stdlib imports
16 16 import atexit
17 17 import fnmatch
18 18 import json
19 19 import os
20 20 import sys
21 21 import threading
22 22 import time
23 23
24 24 # Our own packages
25 25 import IPython.utils.io
26 26
27 27 from IPython.utils.pickleshare import PickleShareDB
28 28 from IPython.utils.io import ask_yes_no
29 29 from IPython.utils.warn import warn
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes and functions
33 33 #-----------------------------------------------------------------------------
34 34
35 35 class HistoryManager(object):
36 36 """A class to organize all history-related functionality in one place.
37 37 """
38 38 # Public interface
39 39
40 40 # An instance of the IPython shell we are attached to
41 41 shell = None
42 42 # A list to hold processed history
43 43 input_hist_parsed = None
44 44 # A list to hold raw history (as typed by user)
45 45 input_hist_raw = None
46 46 # A list of directories visited during session
47 47 dir_hist = None
48 48 # A dict of output history, keyed with ints from the shell's execution count
49 49 output_hist = None
50 50 # String with path to the history file
51 51 hist_file = None
52 52 # PickleShareDB instance holding the raw data for the shadow history
53 53 shadow_db = None
54 54 # ShadowHist instance with the actual shadow history
55 55 shadow_hist = None
56 56
57 57 # Private interface
58 58 # Variables used to store the three last inputs from the user. On each new
59 59 # history update, we populate the user's namespace with these, shifted as
60 60 # necessary.
61 61 _i00, _i, _ii, _iii = '','','',''
62 62
63 63 # A set with all forms of the exit command, so that we don't store them in
64 64 # the history (it's annoying to rewind the first entry and land on an exit
65 65 # call).
66 66 _exit_commands = None
67 67
68 68 def __init__(self, shell):
69 69 """Create a new history manager associated with a shell instance.
70 70 """
71 71 # We need a pointer back to the shell for various tasks.
72 72 self.shell = shell
73 73
74 74 # List of input with multi-line handling.
75 75 self.input_hist_parsed = []
76 76 # This one will hold the 'raw' input history, without any
77 77 # pre-processing. This will allow users to retrieve the input just as
78 78 # it was exactly typed in by the user, with %hist -r.
79 79 self.input_hist_raw = []
80 80
81 81 # list of visited directories
82 82 try:
83 83 self.dir_hist = [os.getcwd()]
84 84 except OSError:
85 85 self.dir_hist = []
86 86
87 87 # dict of output history
88 88 self.output_hist = {}
89 89
90 90 # Now the history file
91 91 if shell.profile:
92 92 histfname = 'history-%s' % shell.profile
93 93 else:
94 94 histfname = 'history'
95 95 self.hist_file = os.path.join(shell.ipython_dir, histfname + '.json')
96 96
97 97 # Objects related to shadow history management
98 98 self._init_shadow_hist()
99 99
100 100 self._i00, self._i, self._ii, self._iii = '','','',''
101 101
102 102 self._exit_commands = set(['Quit', 'quit', 'Exit', 'exit', '%Quit',
103 103 '%quit', '%Exit', '%exit'])
104 104
105 105 # Object is fully initialized, we can now call methods on it.
106 106
107 107 # Fill the history zero entry, user counter starts at 1
108 108 self.store_inputs('\n', '\n')
109 109
110 # Autosave every 60 seconds:
110 # Create and start the autosaver.
111 111 self.autosave_flag = threading.Event()
112 112 self.autosave_timer = HistorySaveThread(self.autosave_flag, 60)
113 113 self.autosave_timer.start()
114 # Register the autosave handler to be triggered as a post execute
115 # callback.
114 116 self.shell.register_post_execute(self.autosave_if_due)
115 # Ensure that any autosave thread we start is stopped tidily.
116 117
117 118 def _init_shadow_hist(self):
118 119 try:
119 120 self.shadow_db = PickleShareDB(os.path.join(
120 121 self.shell.ipython_dir, 'db'))
121 122 except UnicodeDecodeError:
122 123 print("Your ipython_dir can't be decoded to unicode!")
123 124 print("Please set HOME environment variable to something that")
124 125 print(r"only has ASCII characters, e.g. c:\home")
125 126 print("Now it is", self.ipython_dir)
126 127 sys.exit()
127 128 self.shadow_hist = ShadowHist(self.shadow_db, self.shell)
128 129
129 130 def populate_readline_history(self):
130 131 """Populate the readline history from the raw history.
131 132
132 133 We only store one copy of the raw history, which is persisted to a json
133 134 file on disk. The readline history is repopulated from the contents of
134 135 this file."""
135 136
136 137 try:
137 138 self.shell.readline.clear_history()
138 139 except AttributeError:
139 140 pass
140 141 else:
141 142 for h in self.input_hist_raw:
142 143 if not h.isspace():
143 144 for line in h.splitlines():
144 145 self.shell.readline.add_history(line)
145 146
146 147 def save_history(self):
147 148 """Save input history to a file (via readline library)."""
148 149 hist = dict(raw=self.input_hist_raw, #[-self.shell.history_length:],
149 150 parsed=self.input_hist_parsed) #[-self.shell.history_length:])
150 151 with open(self.hist_file,'wt') as hfile:
151 152 json.dump(hist, hfile,
152 153 sort_keys=True, indent=4)
153 154
154 155 def autosave_if_due(self):
155 156 """Check if the autosave event is set; if so, save history. We do it
156 157 this way so that the save takes place in the main thread."""
157 158 if self.autosave_flag.is_set():
158 159 self.save_history()
159 160 self.autosave_flag.clear()
160 161
161 162 def reload_history(self):
162 163 """Reload the input history from disk file."""
163 164
164 165 with open(self.hist_file,'rt') as hfile:
165 166 try:
166 167 hist = json.load(hfile)
167 168 except ValueError: # Ignore it if JSON is corrupt.
168 169 return
169 170 self.input_hist_parsed = hist['parsed']
170 171 self.input_hist_raw = hist['raw']
171 172 if self.shell.has_readline:
172 173 self.populate_readline_history()
173 174
174 175 def get_history(self, index=None, raw=False, output=True):
175 176 """Get the history list.
176 177
177 178 Get the input and output history.
178 179
179 180 Parameters
180 181 ----------
181 182 index : n or (n1, n2) or None
182 183 If n, then the last entries. If a tuple, then all in
183 184 range(n1, n2). If None, then all entries. Raises IndexError if
184 185 the format of index is incorrect.
185 186 raw : bool
186 187 If True, return the raw input.
187 188 output : bool
188 189 If True, then return the output as well.
189 190
190 191 Returns
191 192 -------
192 193 If output is True, then return a dict of tuples, keyed by the prompt
193 194 numbers and with values of (input, output). If output is False, then
194 195 a dict, keyed by the prompt number with the values of input. Raises
195 196 IndexError if no history is found.
196 197 """
197 198 if raw:
198 199 input_hist = self.input_hist_raw
199 200 else:
200 201 input_hist = self.input_hist_parsed
201 202 if output:
202 203 output_hist = self.output_hist
203 204 n = len(input_hist)
204 205 if index is None:
205 206 start=0; stop=n
206 207 elif isinstance(index, int):
207 208 start=n-index; stop=n
208 209 elif isinstance(index, tuple) and len(index) == 2:
209 210 start=index[0]; stop=index[1]
210 211 else:
211 212 raise IndexError('Not a valid index for the input history: %r'
212 213 % index)
213 214 hist = {}
214 215 for i in range(start, stop):
215 216 if output:
216 217 hist[i] = (input_hist[i], output_hist.get(i))
217 218 else:
218 219 hist[i] = input_hist[i]
219 220 if not hist:
220 221 raise IndexError('No history for range of indices: %r' % index)
221 222 return hist
222 223
223 224 def store_inputs(self, source, source_raw=None):
224 225 """Store source and raw input in history and create input cache
225 226 variables _i*.
226 227
227 228 Parameters
228 229 ----------
229 230 source : str
230 231 Python input.
231 232
232 233 source_raw : str, optional
233 234 If given, this is the raw input without any IPython transformations
234 235 applied to it. If not given, ``source`` is used.
235 236 """
236 237 if source_raw is None:
237 238 source_raw = source
238 239
239 240 # do not store exit/quit commands
240 241 if source_raw.strip() in self._exit_commands:
241 242 return
242 243
243 244 self.input_hist_parsed.append(source.rstrip())
244 245 self.input_hist_raw.append(source_raw.rstrip())
245 246 self.shadow_hist.add(source)
246 247
247 248 # update the auto _i variables
248 249 self._iii = self._ii
249 250 self._ii = self._i
250 251 self._i = self._i00
251 252 self._i00 = source_raw
252 253
253 254 # hackish access to user namespace to create _i1,_i2... dynamically
254 255 new_i = '_i%s' % self.shell.execution_count
255 256 to_main = {'_i': self._i,
256 257 '_ii': self._ii,
257 258 '_iii': self._iii,
258 259 new_i : self._i00 }
259 260 self.shell.user_ns.update(to_main)
260 261
261 262 def sync_inputs(self):
262 263 """Ensure raw and translated histories have same length."""
263 264 if len(self.input_hist_parsed) != len (self.input_hist_raw):
264 265 self.input_hist_raw[:] = self.input_hist_parsed
265 266
266 267 def reset(self):
267 268 """Clear all histories managed by this object."""
268 269 self.input_hist_parsed[:] = []
269 270 self.input_hist_raw[:] = []
270 271 self.output_hist.clear()
271 272 # The directory history can't be completely empty
272 273 self.dir_hist[:] = [os.getcwd()]
273 274
274 275 class HistorySaveThread(threading.Thread):
275 """This thread makes IPython save history periodically (the current default
276 is once per minute), so that it is not lost in the event of a crash. It also
277 allows the commands in the current IPython shell to be accessed in a newly
278 started instance.
276 """This thread makes IPython save history periodically.
277
278 Without this class, IPython would only save the history on a clean exit.
279 This saves the history periodically (the current default is once per
280 minute), so that it is not lost in the event of a crash.
279 281
280 This simply sets an event to indicate that history should be saved. The
281 actual save is carried out after executing a user command, to avoid
282 thread issues."""
282 The implementation sets an event to indicate that history should be saved.
283 The actual save is carried out after executing a user command, to avoid
284 thread issues.
285 """
283 286 daemon = True
284 287
285 288 def __init__(self, autosave_flag, time_interval=60):
286 289 threading.Thread.__init__(self)
287 290 self.time_interval = time_interval
288 291 self.autosave_flag = autosave_flag
289 292 self.exit_now = threading.Event()
290 293 # Ensure the thread is stopped tidily when exiting normally
291 294 atexit.register(self.stop)
292 295
293 296 def run(self):
294 297 while True:
295 298 self.exit_now.wait(self.time_interval)
296 299 if self.exit_now.is_set():
297 300 break
298 #print("Setting flag for autosaving history...") # DEBUG
299 301 self.autosave_flag.set()
300 302
301 303 def stop(self):
302 304 """Safely and quickly stop the autosave timer thread."""
303 305 self.exit_now.set()
304 306 self.join()
305 307
306 308 def magic_history(self, parameter_s = ''):
307 309 """Print input history (_i<n> variables), with most recent last.
308 310
309 311 %history -> print at most 40 inputs (some may be multi-line)\\
310 312 %history n -> print at most n inputs\\
311 313 %history n1 n2 -> print inputs between n1 and n2 (n2 not included)\\
312 314
313 315 By default, input history is printed without line numbers so it can be
314 316 directly pasted into an editor.
315 317
316 318 With -n, each input's number <n> is shown, and is accessible as the
317 319 automatically generated variable _i<n> as well as In[<n>]. Multi-line
318 320 statements are printed starting at a new line for easy copy/paste.
319 321
320 322 Options:
321 323
322 324 -n: print line numbers for each input.
323 325 This feature is only available if numbered prompts are in use.
324 326
325 327 -o: also print outputs for each input.
326 328
327 329 -p: print classic '>>>' python prompts before each input. This is useful
328 330 for making documentation, and in conjunction with -o, for producing
329 331 doctest-ready output.
330 332
331 333 -r: (default) print the 'raw' history, i.e. the actual commands you typed.
332 334
333 335 -t: print the 'translated' history, as IPython understands it. IPython
334 336 filters your input and converts it all into valid Python source before
335 337 executing it (things like magics or aliases are turned into function
336 338 calls, for example). With this option, you'll see the native history
337 339 instead of the user-entered version: '%cd /' will be seen as
338 340 'get_ipython().magic("%cd /")' instead of '%cd /'.
339 341
340 342 -g: treat the arg as a pattern to grep for in (full) history.
341 343 This includes the "shadow history" (almost all commands ever written).
342 344 Use '%hist -g' to show full shadow history (may be very long).
343 345 In shadow history, every index nuwber starts with 0.
344 346
345 347 -f FILENAME: instead of printing the output to the screen, redirect it to
346 348 the given file. The file is always overwritten, though IPython asks for
347 349 confirmation first if it already exists.
348 350 """
349 351
350 352 if not self.shell.displayhook.do_full_cache:
351 353 print('This feature is only available if numbered prompts are in use.')
352 354 return
353 355 opts,args = self.parse_options(parameter_s,'gnoptsrf:',mode='list')
354 356
355 357 # Check if output to specific file was requested.
356 358 try:
357 359 outfname = opts['f']
358 360 except KeyError:
359 361 outfile = IPython.utils.io.Term.cout # default
360 362 # We don't want to close stdout at the end!
361 363 close_at_end = False
362 364 else:
363 365 if os.path.exists(outfname):
364 366 if not ask_yes_no("File %r exists. Overwrite?" % outfname):
365 367 print('Aborting.')
366 368 return
367 369
368 370 outfile = open(outfname,'w')
369 371 close_at_end = True
370 372
371 373 if 't' in opts:
372 374 input_hist = self.shell.history_manager.input_hist_parsed
373 375 elif 'r' in opts:
374 376 input_hist = self.shell.history_manager.input_hist_raw
375 377 else:
376 378 # Raw history is the default
377 379 input_hist = self.shell.history_manager.input_hist_raw
378 380
379 381 default_length = 40
380 382 pattern = None
381 383 if 'g' in opts:
382 384 init = 1
383 385 final = len(input_hist)
384 386 parts = parameter_s.split(None, 1)
385 387 if len(parts) == 1:
386 388 parts += '*'
387 389 head, pattern = parts
388 390 pattern = "*" + pattern + "*"
389 391 elif len(args) == 0:
390 392 final = len(input_hist)-1
391 393 init = max(1,final-default_length)
392 394 elif len(args) == 1:
393 395 final = len(input_hist)
394 396 init = max(1, final-int(args[0]))
395 397 elif len(args) == 2:
396 398 init, final = map(int, args)
397 399 else:
398 400 warn('%hist takes 0, 1 or 2 arguments separated by spaces.')
399 401 print(self.magic_hist.__doc__, file=IPython.utils.io.Term.cout)
400 402 return
401 403
402 404 width = len(str(final))
403 405 line_sep = ['','\n']
404 406 print_nums = 'n' in opts
405 407 print_outputs = 'o' in opts
406 408 pyprompts = 'p' in opts
407 409
408 410 found = False
409 411 if pattern is not None:
410 412 sh = self.shell.history_manager.shadowhist.all()
411 413 for idx, s in sh:
412 414 if fnmatch.fnmatch(s, pattern):
413 415 print("0%d: %s" %(idx, s.expandtabs(4)), file=outfile)
414 416 found = True
415 417
416 418 if found:
417 419 print("===", file=outfile)
418 420 print("shadow history ends, fetch by %rep <number> (must start with 0)",
419 421 file=outfile)
420 422 print("=== start of normal history ===", file=outfile)
421 423
422 424 for in_num in range(init, final):
423 425 # Print user history with tabs expanded to 4 spaces. The GUI clients
424 426 # use hard tabs for easier usability in auto-indented code, but we want
425 427 # to produce PEP-8 compliant history for safe pasting into an editor.
426 428 inline = input_hist[in_num].expandtabs(4).rstrip()+'\n'
427 429
428 430 if pattern is not None and not fnmatch.fnmatch(inline, pattern):
429 431 continue
430 432
431 433 multiline = int(inline.count('\n') > 1)
432 434 if print_nums:
433 435 print('%s:%s' % (str(in_num).ljust(width), line_sep[multiline]),
434 436 file=outfile)
435 437 if pyprompts:
436 438 print('>>>', file=outfile)
437 439 if multiline:
438 440 lines = inline.splitlines()
439 441 print('\n... '.join(lines), file=outfile)
440 442 print('... ', file=outfile)
441 443 else:
442 444 print(inline, end='', file=outfile)
443 445 else:
444 446 print(inline, end='', file=outfile)
445 447 if print_outputs:
446 448 output = self.shell.history_manager.output_hist.get(in_num)
447 449 if output is not None:
448 450 print(repr(output), file=outfile)
449 451
450 452 if close_at_end:
451 453 outfile.close()
452 454
453 455
454 456 def magic_hist(self, parameter_s=''):
455 457 """Alternate name for %history."""
456 458 return self.magic_history(parameter_s)
457 459
458 460
459 461 def rep_f(self, arg):
460 462 r""" Repeat a command, or get command to input line for editing
461 463
462 464 - %rep (no arguments):
463 465
464 466 Place a string version of last computation result (stored in the special '_'
465 467 variable) to the next input prompt. Allows you to create elaborate command
466 468 lines without using copy-paste::
467 469
468 470 $ l = ["hei", "vaan"]
469 471 $ "".join(l)
470 472 ==> heivaan
471 473 $ %rep
472 474 $ heivaan_ <== cursor blinking
473 475
474 476 %rep 45
475 477
476 478 Place history line 45 to next input prompt. Use %hist to find out the
477 479 number.
478 480
479 481 %rep 1-4 6-7 3
480 482
481 483 Repeat the specified lines immediately. Input slice syntax is the same as
482 484 in %macro and %save.
483 485
484 486 %rep foo
485 487
486 488 Place the most recent line that has the substring "foo" to next input.
487 489 (e.g. 'svn ci -m foobar').
488 490 """
489 491
490 492 opts,args = self.parse_options(arg,'',mode='list')
491 493 if not args:
492 494 self.set_next_input(str(self.shell.user_ns["_"]))
493 495 return
494 496
495 497 if len(args) == 1 and not '-' in args[0]:
496 498 arg = args[0]
497 499 if len(arg) > 1 and arg.startswith('0'):
498 500 # get from shadow hist
499 501 num = int(arg[1:])
500 502 line = self.shell.shadowhist.get(num)
501 503 self.set_next_input(str(line))
502 504 return
503 505 try:
504 506 num = int(args[0])
505 507 self.set_next_input(str(self.shell.input_hist_raw[num]).rstrip())
506 508 return
507 509 except ValueError:
508 510 pass
509 511
510 512 for h in reversed(self.shell.input_hist_raw):
511 513 if 'rep' in h:
512 514 continue
513 515 if fnmatch.fnmatch(h,'*' + arg + '*'):
514 516 self.set_next_input(str(h).rstrip())
515 517 return
516 518
517 519 try:
518 520 lines = self.extract_input_slices(args, True)
519 521 print("lines", lines)
520 522 self.run_cell(lines)
521 523 except ValueError:
522 524 print("Not found in recent history:", args)
523 525
524 526
525 527 _sentinel = object()
526 528
527 529 class ShadowHist(object):
528 530 def __init__(self, db, shell):
529 531 # cmd => idx mapping
530 532 self.curidx = 0
531 533 self.db = db
532 534 self.disabled = False
533 535 self.shell = shell
534 536
535 537 def inc_idx(self):
536 538 idx = self.db.get('shadowhist_idx', 1)
537 539 self.db['shadowhist_idx'] = idx + 1
538 540 return idx
539 541
540 542 def add(self, ent):
541 543 if self.disabled:
542 544 return
543 545 try:
544 546 old = self.db.hget('shadowhist', ent, _sentinel)
545 547 if old is not _sentinel:
546 548 return
547 549 newidx = self.inc_idx()
548 550 #print("new", newidx) # dbg
549 551 self.db.hset('shadowhist',ent, newidx)
550 552 except:
551 553 self.shell.showtraceback()
552 554 print("WARNING: disabling shadow history")
553 555 self.disabled = True
554 556
555 557 def all(self):
556 558 d = self.db.hdict('shadowhist')
557 559 items = [(i,s) for (s,i) in d.iteritems()]
558 560 items.sort()
559 561 return items
560 562
561 563 def get(self, idx):
562 564 all = self.all()
563 565
564 566 for k, v in all:
565 567 if k == idx:
566 568 return v
567 569
568 570
569 571 def init_ipython(ip):
570 572 ip.define_magic("rep",rep_f)
571 573 ip.define_magic("hist",magic_hist)
572 574 ip.define_magic("history",magic_history)
573 575
574 576 # XXX - ipy_completers are in quarantine, need to be updated to new apis
575 577 #import ipy_completers
576 578 #ipy_completers.quick_completer('%hist' ,'-g -t -r -n')
General Comments 0
You need to be logged in to leave comments. Login now