##// END OF EJS Templates
Merge pull request #2967 from joonro/history-previous-session-no-line...
Thomas Kluyver -
r9800:f5a6eee5 merge
parent child Browse files
Show More
@@ -1,797 +1,806 b''
1 1 """ History related magics and functionality """
2 2 #-----------------------------------------------------------------------------
3 3 # Copyright (C) 2010-2011 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 datetime
18 18 import os
19 19 import re
20 20 try:
21 21 import sqlite3
22 22 except ImportError:
23 23 try:
24 24 from pysqlite2 import dbapi2 as sqlite3
25 25 except ImportError:
26 26 sqlite3 = None
27 27 import threading
28 28
29 29 # Our own packages
30 30 from IPython.config.configurable import Configurable
31 31 from IPython.external.decorator import decorator
32 32 from IPython.utils.path import locate_profile
33 33 from IPython.utils.traitlets import (
34 34 Any, Bool, Dict, Instance, Integer, List, Unicode, TraitError,
35 35 )
36 36 from IPython.utils.warn import warn
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes and functions
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class DummyDB(object):
43 43 """Dummy DB that will act as a black hole for history.
44 44
45 45 Only used in the absence of sqlite"""
46 46 def execute(*args, **kwargs):
47 47 return []
48 48
49 49 def commit(self, *args, **kwargs):
50 50 pass
51 51
52 52 def __enter__(self, *args, **kwargs):
53 53 pass
54 54
55 55 def __exit__(self, *args, **kwargs):
56 56 pass
57 57
58 58
59 59 @decorator
60 60 def needs_sqlite(f, self, *a, **kw):
61 61 """return an empty list in the absence of sqlite"""
62 62 if sqlite3 is None or not self.enabled:
63 63 return []
64 64 else:
65 65 return f(self, *a, **kw)
66 66
67 67
68 68 if sqlite3 is not None:
69 69 DatabaseError = sqlite3.DatabaseError
70 70 else:
71 71 class DatabaseError(Exception):
72 72 "Dummy exception when sqlite could not be imported. Should never occur."
73 73
74 74 @decorator
75 75 def catch_corrupt_db(f, self, *a, **kw):
76 76 """A decorator which wraps HistoryAccessor method calls to catch errors from
77 77 a corrupt SQLite database, move the old database out of the way, and create
78 78 a new one.
79 79 """
80 80 try:
81 81 return f(self, *a, **kw)
82 82 except DatabaseError:
83 83 if os.path.isfile(self.hist_file):
84 84 # Try to move the file out of the way
85 85 base,ext = os.path.splitext(self.hist_file)
86 86 newpath = base + '-corrupt' + ext
87 87 os.rename(self.hist_file, newpath)
88 88 self.init_db()
89 89 print("ERROR! History file wasn't a valid SQLite database.",
90 90 "It was moved to %s" % newpath, "and a new file created.")
91 91 return []
92 92
93 93 else:
94 94 # The hist_file is probably :memory: or something else.
95 95 raise
96 96
97 97
98 98
99 99 class HistoryAccessor(Configurable):
100 100 """Access the history database without adding to it.
101 101
102 102 This is intended for use by standalone history tools. IPython shells use
103 103 HistoryManager, below, which is a subclass of this."""
104 104
105 105 # String holding the path to the history file
106 106 hist_file = Unicode(config=True,
107 107 help="""Path to file to use for SQLite history database.
108 108
109 109 By default, IPython will put the history database in the IPython
110 110 profile directory. If you would rather share one history among
111 111 profiles, you can set this value in each, so that they are consistent.
112 112
113 113 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
114 114 mounts. If you see IPython hanging, try setting this to something on a
115 115 local disk, e.g::
116 116
117 117 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
118 118
119 119 """)
120 120
121 121 enabled = Bool(True, config=True,
122 122 help="""enable the SQLite history
123 123
124 124 set enabled=False to disable the SQLite history,
125 125 in which case there will be no stored history, no SQLite connection,
126 126 and no background saving thread. This may be necessary in some
127 127 threaded environments where IPython is embedded.
128 128 """
129 129 )
130 130
131 131 connection_options = Dict(config=True,
132 132 help="""Options for configuring the SQLite connection
133 133
134 134 These options are passed as keyword args to sqlite3.connect
135 135 when establishing database conenctions.
136 136 """
137 137 )
138 138
139 139 # The SQLite database
140 140 db = Any()
141 141 def _db_changed(self, name, old, new):
142 142 """validate the db, since it can be an Instance of two different types"""
143 143 connection_types = (DummyDB,)
144 144 if sqlite3 is not None:
145 145 connection_types = (DummyDB, sqlite3.Connection)
146 146 if not isinstance(new, connection_types):
147 147 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
148 148 (self.__class__.__name__, new)
149 149 raise TraitError(msg)
150 150
151 151 def __init__(self, profile='default', hist_file=u'', config=None, **traits):
152 152 """Create a new history accessor.
153 153
154 154 Parameters
155 155 ----------
156 156 profile : str
157 157 The name of the profile from which to open history.
158 158 hist_file : str
159 159 Path to an SQLite history database stored by IPython. If specified,
160 160 hist_file overrides profile.
161 161 config :
162 162 Config object. hist_file can also be set through this.
163 163 """
164 164 # We need a pointer back to the shell for various tasks.
165 165 super(HistoryAccessor, self).__init__(config=config, **traits)
166 166 # defer setting hist_file from kwarg until after init,
167 167 # otherwise the default kwarg value would clobber any value
168 168 # set by config
169 169 if hist_file:
170 170 self.hist_file = hist_file
171 171
172 172 if self.hist_file == u'':
173 173 # No one has set the hist_file, yet.
174 174 self.hist_file = self._get_hist_file_name(profile)
175 175
176 176 if sqlite3 is None and self.enabled:
177 177 warn("IPython History requires SQLite, your history will not be saved")
178 178 self.enabled = False
179 179
180 180 self.init_db()
181 181
182 182 def _get_hist_file_name(self, profile='default'):
183 183 """Find the history file for the given profile name.
184 184
185 185 This is overridden by the HistoryManager subclass, to use the shell's
186 186 active profile.
187 187
188 188 Parameters
189 189 ----------
190 190 profile : str
191 191 The name of a profile which has a history file.
192 192 """
193 193 return os.path.join(locate_profile(profile), 'history.sqlite')
194 194
195 195 @catch_corrupt_db
196 196 def init_db(self):
197 197 """Connect to the database, and create tables if necessary."""
198 198 if not self.enabled:
199 199 self.db = DummyDB()
200 200 return
201 201
202 202 # use detect_types so that timestamps return datetime objects
203 203 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
204 204 kwargs.update(self.connection_options)
205 205 self.db = sqlite3.connect(self.hist_file, **kwargs)
206 206 self.db.execute("""CREATE TABLE IF NOT EXISTS sessions (session integer
207 207 primary key autoincrement, start timestamp,
208 208 end timestamp, num_cmds integer, remark text)""")
209 209 self.db.execute("""CREATE TABLE IF NOT EXISTS history
210 210 (session integer, line integer, source text, source_raw text,
211 211 PRIMARY KEY (session, line))""")
212 212 # Output history is optional, but ensure the table's there so it can be
213 213 # enabled later.
214 214 self.db.execute("""CREATE TABLE IF NOT EXISTS output_history
215 215 (session integer, line integer, output text,
216 216 PRIMARY KEY (session, line))""")
217 217 self.db.commit()
218 218
219 219 def writeout_cache(self):
220 220 """Overridden by HistoryManager to dump the cache before certain
221 221 database lookups."""
222 222 pass
223 223
224 224 ## -------------------------------
225 225 ## Methods for retrieving history:
226 226 ## -------------------------------
227 227 def _run_sql(self, sql, params, raw=True, output=False):
228 228 """Prepares and runs an SQL query for the history database.
229 229
230 230 Parameters
231 231 ----------
232 232 sql : str
233 233 Any filtering expressions to go after SELECT ... FROM ...
234 234 params : tuple
235 235 Parameters passed to the SQL query (to replace "?")
236 236 raw, output : bool
237 237 See :meth:`get_range`
238 238
239 239 Returns
240 240 -------
241 241 Tuples as :meth:`get_range`
242 242 """
243 243 toget = 'source_raw' if raw else 'source'
244 244 sqlfrom = "history"
245 245 if output:
246 246 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
247 247 toget = "history.%s, output_history.output" % toget
248 248 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
249 249 (toget, sqlfrom) + sql, params)
250 250 if output: # Regroup into 3-tuples, and parse JSON
251 251 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
252 252 return cur
253 253
254 254 @needs_sqlite
255 255 @catch_corrupt_db
256 256 def get_session_info(self, session=0):
257 257 """get info about a session
258 258
259 259 Parameters
260 260 ----------
261 261
262 262 session : int
263 263 Session number to retrieve. The current session is 0, and negative
264 264 numbers count back from current session, so -1 is previous session.
265 265
266 266 Returns
267 267 -------
268 268
269 269 (session_id [int], start [datetime], end [datetime], num_cmds [int],
270 270 remark [unicode])
271 271
272 272 Sessions that are running or did not exit cleanly will have `end=None`
273 273 and `num_cmds=None`.
274 274
275 275 """
276 276
277 277 if session <= 0:
278 278 session += self.session_number
279 279
280 280 query = "SELECT * from sessions where session == ?"
281 281 return self.db.execute(query, (session,)).fetchone()
282 282
283 283 @catch_corrupt_db
284 284 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
285 285 """Get the last n lines from the history database.
286 286
287 287 Parameters
288 288 ----------
289 289 n : int
290 290 The number of lines to get
291 291 raw, output : bool
292 292 See :meth:`get_range`
293 293 include_latest : bool
294 294 If False (default), n+1 lines are fetched, and the latest one
295 295 is discarded. This is intended to be used where the function
296 296 is called by a user command, which it should not return.
297 297
298 298 Returns
299 299 -------
300 300 Tuples as :meth:`get_range`
301 301 """
302 302 self.writeout_cache()
303 303 if not include_latest:
304 304 n += 1
305 305 cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
306 306 (n,), raw=raw, output=output)
307 307 if not include_latest:
308 308 return reversed(list(cur)[1:])
309 309 return reversed(list(cur))
310 310
311 311 @catch_corrupt_db
312 312 def search(self, pattern="*", raw=True, search_raw=True,
313 313 output=False, n=None, unique=False):
314 314 """Search the database using unix glob-style matching (wildcards
315 315 * and ?).
316 316
317 317 Parameters
318 318 ----------
319 319 pattern : str
320 320 The wildcarded pattern to match when searching
321 321 search_raw : bool
322 322 If True, search the raw input, otherwise, the parsed input
323 323 raw, output : bool
324 324 See :meth:`get_range`
325 325 n : None or int
326 326 If an integer is given, it defines the limit of
327 327 returned entries.
328 328 unique : bool
329 329 When it is true, return only unique entries.
330 330
331 331 Returns
332 332 -------
333 333 Tuples as :meth:`get_range`
334 334 """
335 335 tosearch = "source_raw" if search_raw else "source"
336 336 if output:
337 337 tosearch = "history." + tosearch
338 338 self.writeout_cache()
339 339 sqlform = "WHERE %s GLOB ?" % tosearch
340 340 params = (pattern,)
341 341 if unique:
342 342 sqlform += ' GROUP BY {0}'.format(tosearch)
343 343 if n is not None:
344 344 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
345 345 params += (n,)
346 346 elif unique:
347 347 sqlform += " ORDER BY session, line"
348 348 cur = self._run_sql(sqlform, params, raw=raw, output=output)
349 349 if n is not None:
350 350 return reversed(list(cur))
351 351 return cur
352 352
353 353 @catch_corrupt_db
354 354 def get_range(self, session, start=1, stop=None, raw=True,output=False):
355 355 """Retrieve input by session.
356 356
357 357 Parameters
358 358 ----------
359 359 session : int
360 360 Session number to retrieve.
361 361 start : int
362 362 First line to retrieve.
363 363 stop : int
364 364 End of line range (excluded from output itself). If None, retrieve
365 365 to the end of the session.
366 366 raw : bool
367 367 If True, return untranslated input
368 368 output : bool
369 369 If True, attempt to include output. This will be 'real' Python
370 370 objects for the current session, or text reprs from previous
371 371 sessions if db_log_output was enabled at the time. Where no output
372 372 is found, None is used.
373 373
374 374 Returns
375 375 -------
376 376 An iterator over the desired lines. Each line is a 3-tuple, either
377 377 (session, line, input) if output is False, or
378 378 (session, line, (input, output)) if output is True.
379 379 """
380 380 if stop:
381 381 lineclause = "line >= ? AND line < ?"
382 382 params = (session, start, stop)
383 383 else:
384 384 lineclause = "line>=?"
385 385 params = (session, start)
386 386
387 387 return self._run_sql("WHERE session==? AND %s" % lineclause,
388 388 params, raw=raw, output=output)
389 389
390 390 def get_range_by_str(self, rangestr, raw=True, output=False):
391 391 """Get lines of history from a string of ranges, as used by magic
392 392 commands %hist, %save, %macro, etc.
393 393
394 394 Parameters
395 395 ----------
396 396 rangestr : str
397 397 A string specifying ranges, e.g. "5 ~2/1-4". See
398 398 :func:`magic_history` for full details.
399 399 raw, output : bool
400 400 As :meth:`get_range`
401 401
402 402 Returns
403 403 -------
404 404 Tuples as :meth:`get_range`
405 405 """
406 406 for sess, s, e in extract_hist_ranges(rangestr):
407 407 for line in self.get_range(sess, s, e, raw=raw, output=output):
408 408 yield line
409 409
410 410
411 411 class HistoryManager(HistoryAccessor):
412 412 """A class to organize all history-related functionality in one place.
413 413 """
414 414 # Public interface
415 415
416 416 # An instance of the IPython shell we are attached to
417 417 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
418 418 # Lists to hold processed and raw history. These start with a blank entry
419 419 # so that we can index them starting from 1
420 420 input_hist_parsed = List([""])
421 421 input_hist_raw = List([""])
422 422 # A list of directories visited during session
423 423 dir_hist = List()
424 424 def _dir_hist_default(self):
425 425 try:
426 426 return [os.getcwdu()]
427 427 except OSError:
428 428 return []
429 429
430 430 # A dict of output history, keyed with ints from the shell's
431 431 # execution count.
432 432 output_hist = Dict()
433 433 # The text/plain repr of outputs.
434 434 output_hist_reprs = Dict()
435 435
436 436 # The number of the current session in the history database
437 437 session_number = Integer()
438 438 # Should we log output to the database? (default no)
439 439 db_log_output = Bool(False, config=True)
440 440 # Write to database every x commands (higher values save disk access & power)
441 441 # Values of 1 or less effectively disable caching.
442 442 db_cache_size = Integer(0, config=True)
443 443 # The input and output caches
444 444 db_input_cache = List()
445 445 db_output_cache = List()
446 446
447 447 # History saving in separate thread
448 448 save_thread = Instance('IPython.core.history.HistorySavingThread')
449 449 try: # Event is a function returning an instance of _Event...
450 450 save_flag = Instance(threading._Event)
451 451 except AttributeError: # ...until Python 3.3, when it's a class.
452 452 save_flag = Instance(threading.Event)
453 453
454 454 # Private interface
455 455 # Variables used to store the three last inputs from the user. On each new
456 456 # history update, we populate the user's namespace with these, shifted as
457 457 # necessary.
458 458 _i00 = Unicode(u'')
459 459 _i = Unicode(u'')
460 460 _ii = Unicode(u'')
461 461 _iii = Unicode(u'')
462 462
463 463 # A regex matching all forms of the exit command, so that we don't store
464 464 # them in the history (it's annoying to rewind the first entry and land on
465 465 # an exit call).
466 466 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
467 467
468 468 def __init__(self, shell=None, config=None, **traits):
469 469 """Create a new history manager associated with a shell instance.
470 470 """
471 471 # We need a pointer back to the shell for various tasks.
472 472 super(HistoryManager, self).__init__(shell=shell, config=config,
473 473 **traits)
474 474 self.save_flag = threading.Event()
475 475 self.db_input_cache_lock = threading.Lock()
476 476 self.db_output_cache_lock = threading.Lock()
477 477 if self.enabled and self.hist_file != ':memory:':
478 478 self.save_thread = HistorySavingThread(self)
479 479 self.save_thread.start()
480 480
481 481 self.new_session()
482 482
483 483 def _get_hist_file_name(self, profile=None):
484 484 """Get default history file name based on the Shell's profile.
485 485
486 486 The profile parameter is ignored, but must exist for compatibility with
487 487 the parent class."""
488 488 profile_dir = self.shell.profile_dir.location
489 489 return os.path.join(profile_dir, 'history.sqlite')
490 490
491 491 @needs_sqlite
492 492 def new_session(self, conn=None):
493 493 """Get a new session number."""
494 494 if conn is None:
495 495 conn = self.db
496 496
497 497 with conn:
498 498 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
499 499 NULL, "") """, (datetime.datetime.now(),))
500 500 self.session_number = cur.lastrowid
501 501
502 502 def end_session(self):
503 503 """Close the database session, filling in the end time and line count."""
504 504 self.writeout_cache()
505 505 with self.db:
506 506 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
507 507 session==?""", (datetime.datetime.now(),
508 508 len(self.input_hist_parsed)-1, self.session_number))
509 509 self.session_number = 0
510 510
511 511 def name_session(self, name):
512 512 """Give the current session a name in the history database."""
513 513 with self.db:
514 514 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
515 515 (name, self.session_number))
516 516
517 517 def reset(self, new_session=True):
518 518 """Clear the session history, releasing all object references, and
519 519 optionally open a new session."""
520 520 self.output_hist.clear()
521 521 # The directory history can't be completely empty
522 522 self.dir_hist[:] = [os.getcwdu()]
523 523
524 524 if new_session:
525 525 if self.session_number:
526 526 self.end_session()
527 527 self.input_hist_parsed[:] = [""]
528 528 self.input_hist_raw[:] = [""]
529 529 self.new_session()
530 530
531 531 # ------------------------------
532 532 # Methods for retrieving history
533 533 # ------------------------------
534 534 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
535 535 """Get input and output history from the current session. Called by
536 536 get_range, and takes similar parameters."""
537 537 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
538 538
539 539 n = len(input_hist)
540 540 if start < 0:
541 541 start += n
542 542 if not stop or (stop > n):
543 543 stop = n
544 544 elif stop < 0:
545 545 stop += n
546 546
547 547 for i in range(start, stop):
548 548 if output:
549 549 line = (input_hist[i], self.output_hist_reprs.get(i))
550 550 else:
551 551 line = input_hist[i]
552 552 yield (0, i, line)
553 553
554 554 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
555 555 """Retrieve input by session.
556 556
557 557 Parameters
558 558 ----------
559 559 session : int
560 560 Session number to retrieve. The current session is 0, and negative
561 561 numbers count back from current session, so -1 is previous session.
562 562 start : int
563 563 First line to retrieve.
564 564 stop : int
565 565 End of line range (excluded from output itself). If None, retrieve
566 566 to the end of the session.
567 567 raw : bool
568 568 If True, return untranslated input
569 569 output : bool
570 570 If True, attempt to include output. This will be 'real' Python
571 571 objects for the current session, or text reprs from previous
572 572 sessions if db_log_output was enabled at the time. Where no output
573 573 is found, None is used.
574 574
575 575 Returns
576 576 -------
577 577 An iterator over the desired lines. Each line is a 3-tuple, either
578 578 (session, line, input) if output is False, or
579 579 (session, line, (input, output)) if output is True.
580 580 """
581 581 if session <= 0:
582 582 session += self.session_number
583 583 if session==self.session_number: # Current session
584 584 return self._get_range_session(start, stop, raw, output)
585 585 return super(HistoryManager, self).get_range(session, start, stop, raw,
586 586 output)
587 587
588 588 ## ----------------------------
589 589 ## Methods for storing history:
590 590 ## ----------------------------
591 591 def store_inputs(self, line_num, source, source_raw=None):
592 592 """Store source and raw input in history and create input cache
593 593 variables _i*.
594 594
595 595 Parameters
596 596 ----------
597 597 line_num : int
598 598 The prompt number of this input.
599 599
600 600 source : str
601 601 Python input.
602 602
603 603 source_raw : str, optional
604 604 If given, this is the raw input without any IPython transformations
605 605 applied to it. If not given, ``source`` is used.
606 606 """
607 607 if source_raw is None:
608 608 source_raw = source
609 609 source = source.rstrip('\n')
610 610 source_raw = source_raw.rstrip('\n')
611 611
612 612 # do not store exit/quit commands
613 613 if self._exit_re.match(source_raw.strip()):
614 614 return
615 615
616 616 self.input_hist_parsed.append(source)
617 617 self.input_hist_raw.append(source_raw)
618 618
619 619 with self.db_input_cache_lock:
620 620 self.db_input_cache.append((line_num, source, source_raw))
621 621 # Trigger to flush cache and write to DB.
622 622 if len(self.db_input_cache) >= self.db_cache_size:
623 623 self.save_flag.set()
624 624
625 625 # update the auto _i variables
626 626 self._iii = self._ii
627 627 self._ii = self._i
628 628 self._i = self._i00
629 629 self._i00 = source_raw
630 630
631 631 # hackish access to user namespace to create _i1,_i2... dynamically
632 632 new_i = '_i%s' % line_num
633 633 to_main = {'_i': self._i,
634 634 '_ii': self._ii,
635 635 '_iii': self._iii,
636 636 new_i : self._i00 }
637 637
638 638 self.shell.push(to_main, interactive=False)
639 639
640 640 def store_output(self, line_num):
641 641 """If database output logging is enabled, this saves all the
642 642 outputs from the indicated prompt number to the database. It's
643 643 called by run_cell after code has been executed.
644 644
645 645 Parameters
646 646 ----------
647 647 line_num : int
648 648 The line number from which to save outputs
649 649 """
650 650 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
651 651 return
652 652 output = self.output_hist_reprs[line_num]
653 653
654 654 with self.db_output_cache_lock:
655 655 self.db_output_cache.append((line_num, output))
656 656 if self.db_cache_size <= 1:
657 657 self.save_flag.set()
658 658
659 659 def _writeout_input_cache(self, conn):
660 660 with conn:
661 661 for line in self.db_input_cache:
662 662 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
663 663 (self.session_number,)+line)
664 664
665 665 def _writeout_output_cache(self, conn):
666 666 with conn:
667 667 for line in self.db_output_cache:
668 668 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
669 669 (self.session_number,)+line)
670 670
671 671 @needs_sqlite
672 672 def writeout_cache(self, conn=None):
673 673 """Write any entries in the cache to the database."""
674 674 if conn is None:
675 675 conn = self.db
676 676
677 677 with self.db_input_cache_lock:
678 678 try:
679 679 self._writeout_input_cache(conn)
680 680 except sqlite3.IntegrityError:
681 681 self.new_session(conn)
682 682 print("ERROR! Session/line number was not unique in",
683 683 "database. History logging moved to new session",
684 684 self.session_number)
685 685 try:
686 686 # Try writing to the new session. If this fails, don't
687 687 # recurse
688 688 self._writeout_input_cache(conn)
689 689 except sqlite3.IntegrityError:
690 690 pass
691 691 finally:
692 692 self.db_input_cache = []
693 693
694 694 with self.db_output_cache_lock:
695 695 try:
696 696 self._writeout_output_cache(conn)
697 697 except sqlite3.IntegrityError:
698 698 print("!! Session/line number for output was not unique",
699 699 "in database. Output will not be stored.")
700 700 finally:
701 701 self.db_output_cache = []
702 702
703 703
704 704 class HistorySavingThread(threading.Thread):
705 705 """This thread takes care of writing history to the database, so that
706 706 the UI isn't held up while that happens.
707 707
708 708 It waits for the HistoryManager's save_flag to be set, then writes out
709 709 the history cache. The main thread is responsible for setting the flag when
710 710 the cache size reaches a defined threshold."""
711 711 daemon = True
712 712 stop_now = False
713 713 enabled = True
714 714 def __init__(self, history_manager):
715 715 super(HistorySavingThread, self).__init__()
716 716 self.history_manager = history_manager
717 717 self.enabled = history_manager.enabled
718 718 atexit.register(self.stop)
719 719
720 720 @needs_sqlite
721 721 def run(self):
722 722 # We need a separate db connection per thread:
723 723 try:
724 724 self.db = sqlite3.connect(self.history_manager.hist_file,
725 725 **self.history_manager.connection_options
726 726 )
727 727 while True:
728 728 self.history_manager.save_flag.wait()
729 729 if self.stop_now:
730 730 return
731 731 self.history_manager.save_flag.clear()
732 732 self.history_manager.writeout_cache(self.db)
733 733 except Exception as e:
734 734 print(("The history saving thread hit an unexpected error (%s)."
735 735 "History will not be written to the database.") % repr(e))
736 736
737 737 def stop(self):
738 738 """This can be called from the main thread to safely stop this thread.
739 739
740 740 Note that it does not attempt to write out remaining history before
741 741 exiting. That should be done by calling the HistoryManager's
742 742 end_session method."""
743 743 self.stop_now = True
744 744 self.history_manager.save_flag.set()
745 745 self.join()
746 746
747 747
748 748 # To match, e.g. ~5/8-~2/3
749 749 range_re = re.compile(r"""
750 750 ((?P<startsess>~?\d+)/)?
751 (?P<start>\d+) # Only the start line num is compulsory
751 (?P<start>\d+)?
752 752 ((?P<sep>[\-:])
753 753 ((?P<endsess>~?\d+)/)?
754 754 (?P<end>\d+))?
755 755 $""", re.VERBOSE)
756 756
757 757
758 758 def extract_hist_ranges(ranges_str):
759 759 """Turn a string of history ranges into 3-tuples of (session, start, stop).
760 760
761 761 Examples
762 762 --------
763 763 list(extract_input_ranges("~8/5-~7/4 2"))
764 764 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
765 765 """
766 766 for range_str in ranges_str.split():
767 767 rmatch = range_re.match(range_str)
768 768 if not rmatch:
769 769 continue
770 start = int(rmatch.group("start"))
771 end = rmatch.group("end")
772 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
770 start = rmatch.group("start")
771 if start:
772 start = int(start)
773 end = rmatch.group("end")
774 # If no end specified, get (a, a + 1)
775 end = int(end) if end else start + 1
776 else: # start not specified
777 if not rmatch.group('startsess'): # no startsess
778 continue
779 start = 1
780 end = None # provide the entire session hist
781
773 782 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
774 783 end += 1
775 784 startsess = rmatch.group("startsess") or "0"
776 785 endsess = rmatch.group("endsess") or startsess
777 786 startsess = int(startsess.replace("~","-"))
778 787 endsess = int(endsess.replace("~","-"))
779 788 assert endsess >= startsess, "start session must be earlier than end session"
780 789
781 790 if endsess == startsess:
782 791 yield (startsess, start, end)
783 792 continue
784 793 # Multiple sessions in one range:
785 794 yield (startsess, start, None)
786 795 for sess in range(startsess+1, endsess):
787 796 yield (sess, 1, None)
788 797 yield (endsess, 1, end)
789 798
790 799
791 800 def _format_lineno(session, line):
792 801 """Helper function to format line numbers properly."""
793 802 if session == 0:
794 803 return str(line)
795 804 return "%s#%s" % (session, line)
796 805
797 806
@@ -1,184 +1,185 b''
1 1 # coding: utf-8
2 2 """Tests for the IPython tab-completion machinery.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Module imports
6 6 #-----------------------------------------------------------------------------
7 7
8 8 # stdlib
9 9 import os
10 10 import shutil
11 11 import sys
12 12 import tempfile
13 13 import unittest
14 14 from datetime import datetime
15 15
16 16 # third party
17 17 import nose.tools as nt
18 18
19 19 # our own packages
20 20 from IPython.config.loader import Config
21 21 from IPython.utils.tempdir import TemporaryDirectory
22 22 from IPython.core.history import HistoryManager, extract_hist_ranges
23 23 from IPython.utils import py3compat
24 24
25 25 def setUp():
26 26 nt.assert_equal(sys.getdefaultencoding(), "utf-8" if py3compat.PY3 else "ascii")
27 27
28 28 def test_history():
29 29 ip = get_ipython()
30 30 with TemporaryDirectory() as tmpdir:
31 31 hist_manager_ori = ip.history_manager
32 32 hist_file = os.path.join(tmpdir, 'history.sqlite')
33 33 try:
34 34 ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file)
35 35 hist = [u'a=1', u'def f():\n test = 1\n return test', u"b='β‚¬Γ†ΒΎΓ·ΓŸ'"]
36 36 for i, h in enumerate(hist, start=1):
37 37 ip.history_manager.store_inputs(i, h)
38 38
39 39 ip.history_manager.db_log_output = True
40 40 # Doesn't match the input, but we'll just check it's stored.
41 41 ip.history_manager.output_hist_reprs[3] = "spam"
42 42 ip.history_manager.store_output(3)
43 43
44 44 nt.assert_equal(ip.history_manager.input_hist_raw, [''] + hist)
45 45
46 46 # Detailed tests for _get_range_session
47 47 grs = ip.history_manager._get_range_session
48 48 nt.assert_equal(list(grs(start=2,stop=-1)), zip([0], [2], hist[1:-1]))
49 49 nt.assert_equal(list(grs(start=-2)), zip([0,0], [2,3], hist[-2:]))
50 50 nt.assert_equal(list(grs(output=True)), zip([0,0,0], [1,2,3], zip(hist, [None,None,'spam'])))
51 51
52 52 # Check whether specifying a range beyond the end of the current
53 53 # session results in an error (gh-804)
54 54 ip.magic('%hist 2-500')
55 55
56 56 # Check that we can write non-ascii characters to a file
57 57 ip.magic("%%hist -f %s" % os.path.join(tmpdir, "test1"))
58 58 ip.magic("%%hist -pf %s" % os.path.join(tmpdir, "test2"))
59 59 ip.magic("%%hist -nf %s" % os.path.join(tmpdir, "test3"))
60 60 ip.magic("%%save %s 1-10" % os.path.join(tmpdir, "test4"))
61 61
62 62 # New session
63 63 ip.history_manager.reset()
64 64 newcmds = [u"z=5",
65 65 u"class X(object):\n pass",
66 66 u"k='p'",
67 67 u"z=5"]
68 68 for i, cmd in enumerate(newcmds, start=1):
69 69 ip.history_manager.store_inputs(i, cmd)
70 70 gothist = ip.history_manager.get_range(start=1, stop=4)
71 71 nt.assert_equal(list(gothist), zip([0,0,0],[1,2,3], newcmds))
72 72 # Previous session:
73 73 gothist = ip.history_manager.get_range(-1, 1, 4)
74 74 nt.assert_equal(list(gothist), zip([1,1,1],[1,2,3], hist))
75 75
76 76 newhist = [(2, i, c) for (i, c) in enumerate(newcmds, 1)]
77 77
78 78 # Check get_hist_tail
79 79 gothist = ip.history_manager.get_tail(5, output=True,
80 80 include_latest=True)
81 81 expected = [(1, 3, (hist[-1], "spam"))] \
82 82 + [(s, n, (c, None)) for (s, n, c) in newhist]
83 83 nt.assert_equal(list(gothist), expected)
84 84
85 85 gothist = ip.history_manager.get_tail(2)
86 86 expected = newhist[-3:-1]
87 87 nt.assert_equal(list(gothist), expected)
88 88
89 89 # Check get_hist_search
90 90 gothist = ip.history_manager.search("*test*")
91 91 nt.assert_equal(list(gothist), [(1,2,hist[1])] )
92 92
93 93 gothist = ip.history_manager.search("*=*")
94 94 nt.assert_equal(list(gothist),
95 95 [(1, 1, hist[0]),
96 96 (1, 2, hist[1]),
97 97 (1, 3, hist[2]),
98 98 newhist[0],
99 99 newhist[2],
100 100 newhist[3]])
101 101
102 102 gothist = ip.history_manager.search("*=*", n=4)
103 103 nt.assert_equal(list(gothist),
104 104 [(1, 3, hist[2]),
105 105 newhist[0],
106 106 newhist[2],
107 107 newhist[3]])
108 108
109 109 gothist = ip.history_manager.search("*=*", unique=True)
110 110 nt.assert_equal(list(gothist),
111 111 [(1, 1, hist[0]),
112 112 (1, 2, hist[1]),
113 113 (1, 3, hist[2]),
114 114 newhist[2],
115 115 newhist[3]])
116 116
117 117 gothist = ip.history_manager.search("*=*", unique=True, n=3)
118 118 nt.assert_equal(list(gothist),
119 119 [(1, 3, hist[2]),
120 120 newhist[2],
121 121 newhist[3]])
122 122
123 123 gothist = ip.history_manager.search("b*", output=True)
124 124 nt.assert_equal(list(gothist), [(1,3,(hist[2],"spam"))] )
125 125
126 126 # Cross testing: check that magic %save can get previous session.
127 127 testfilename = os.path.realpath(os.path.join(tmpdir, "test.py"))
128 128 ip.magic("save " + testfilename + " ~1/1-3")
129 129 with py3compat.open(testfilename, encoding='utf-8') as testfile:
130 130 nt.assert_equal(testfile.read(),
131 131 u"# coding: utf-8\n" + u"\n".join(hist)+u"\n")
132 132
133 133 # Duplicate line numbers - check that it doesn't crash, and
134 134 # gets a new session
135 135 ip.history_manager.store_inputs(1, "rogue")
136 136 ip.history_manager.writeout_cache()
137 137 nt.assert_equal(ip.history_manager.session_number, 3)
138 138 finally:
139 139 # Restore history manager
140 140 ip.history_manager = hist_manager_ori
141 141
142 142
143 143 def test_extract_hist_ranges():
144 instr = "1 2/3 ~4/5-6 ~4/7-~4/9 ~9/2-~7/5"
144 instr = "1 2/3 ~4/5-6 ~4/7-~4/9 ~9/2-~7/5 ~10/"
145 145 expected = [(0, 1, 2), # 0 == current session
146 146 (2, 3, 4),
147 147 (-4, 5, 7),
148 148 (-4, 7, 10),
149 149 (-9, 2, None), # None == to end
150 150 (-8, 1, None),
151 (-7, 1, 6)]
151 (-7, 1, 6),
152 (-10, 1, None)]
152 153 actual = list(extract_hist_ranges(instr))
153 154 nt.assert_equal(actual, expected)
154 155
155 156 def test_magic_rerun():
156 157 """Simple test for %rerun (no args -> rerun last line)"""
157 158 ip = get_ipython()
158 159 ip.run_cell("a = 10", store_history=True)
159 160 ip.run_cell("a += 1", store_history=True)
160 161 nt.assert_equal(ip.user_ns["a"], 11)
161 162 ip.run_cell("%rerun", store_history=True)
162 163 nt.assert_equal(ip.user_ns["a"], 12)
163 164
164 165 def test_timestamp_type():
165 166 ip = get_ipython()
166 167 info = ip.history_manager.get_session_info()
167 168 nt.assert_true(isinstance(info[1], datetime))
168 169
169 170 def test_hist_file_config():
170 171 cfg = Config()
171 172 tfile = tempfile.NamedTemporaryFile(delete=False)
172 173 cfg.HistoryManager.hist_file = tfile.name
173 174 try:
174 175 hm = HistoryManager(shell=get_ipython(), config=cfg)
175 176 nt.assert_equal(hm.hist_file, cfg.HistoryManager.hist_file)
176 177 finally:
177 178 try:
178 179 os.remove(tfile.name)
179 180 except OSError:
180 181 # same catch as in testing.tools.TempFileMixin
181 182 # On Windows, even though we close the file, we still can't
182 183 # delete it. I have no clue why
183 184 pass
184 185
General Comments 0
You need to be logged in to leave comments. Login now