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