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