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