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