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.4.4, created at 2024-04-20 06:09 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-20 06:09 +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(
571 """INSERT INTO sessions VALUES (NULL, ?, NULL,
572 NULL, '') """,
573 (datetime.datetime.now(),),
574 )
575 self.session_number = cur.lastrowid
577 def end_session(self):
578 """Close the database session, filling in the end time and line count."""
579 self.writeout_cache()
580 with self.db:
581 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
582 session==?""", (datetime.datetime.now(),
583 len(self.input_hist_parsed)-1, self.session_number))
584 self.session_number = 0
586 def name_session(self, name):
587 """Give the current session a name in the history database."""
588 with self.db:
589 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
590 (name, self.session_number))
592 def reset(self, new_session=True):
593 """Clear the session history, releasing all object references, and
594 optionally open a new session."""
595 self.output_hist.clear()
596 # The directory history can't be completely empty
597 self.dir_hist[:] = [Path.cwd()]
599 if new_session:
600 if self.session_number:
601 self.end_session()
602 self.input_hist_parsed[:] = [""]
603 self.input_hist_raw[:] = [""]
604 self.new_session()
606 # ------------------------------
607 # Methods for retrieving history
608 # ------------------------------
609 def get_session_info(self, session=0):
610 """Get info about a session.
612 Parameters
613 ----------
614 session : int
615 Session number to retrieve. The current session is 0, and negative
616 numbers count back from current session, so -1 is the previous session.
618 Returns
619 -------
620 session_id : int
621 Session ID number
622 start : datetime
623 Timestamp for the start of the session.
624 end : datetime
625 Timestamp for the end of the session, or None if IPython crashed.
626 num_cmds : int
627 Number of commands run, or None if IPython crashed.
628 remark : unicode
629 A manually set description.
630 """
631 if session <= 0:
632 session += self.session_number
634 return super(HistoryManager, self).get_session_info(session=session)
636 @catch_corrupt_db
637 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
638 """Get the last n lines from the history database.
640 Most recent entry last.
642 Completion will be reordered so that that the last ones are when
643 possible from current session.
645 Parameters
646 ----------
647 n : int
648 The number of lines to get
649 raw, output : bool
650 See :meth:`get_range`
651 include_latest : bool
652 If False (default), n+1 lines are fetched, and the latest one
653 is discarded. This is intended to be used where the function
654 is called by a user command, which it should not return.
656 Returns
657 -------
658 Tuples as :meth:`get_range`
659 """
660 self.writeout_cache()
661 if not include_latest:
662 n += 1
663 # cursor/line/entry
664 this_cur = list(
665 self._run_sql(
666 "WHERE session == ? ORDER BY line DESC LIMIT ? ",
667 (self.session_number, n),
668 raw=raw,
669 output=output,
670 )
671 )
672 other_cur = list(
673 self._run_sql(
674 "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?",
675 (self.session_number, n),
676 raw=raw,
677 output=output,
678 )
679 )
681 everything = this_cur + other_cur
683 everything = everything[:n]
685 if not include_latest:
686 return list(everything)[:0:-1]
687 return list(everything)[::-1]
689 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
690 """Get input and output history from the current session. Called by
691 get_range, and takes similar parameters."""
692 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
694 n = len(input_hist)
695 if start < 0:
696 start += n
697 if not stop or (stop > n):
698 stop = n
699 elif stop < 0:
700 stop += n
702 for i in range(start, stop):
703 if output:
704 line = (input_hist[i], self.output_hist_reprs.get(i))
705 else:
706 line = input_hist[i]
707 yield (0, i, line)
709 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
710 """Retrieve input by session.
712 Parameters
713 ----------
714 session : int
715 Session number to retrieve. The current session is 0, and negative
716 numbers count back from current session, so -1 is previous session.
717 start : int
718 First line to retrieve.
719 stop : int
720 End of line range (excluded from output itself). If None, retrieve
721 to the end of the session.
722 raw : bool
723 If True, return untranslated input
724 output : bool
725 If True, attempt to include output. This will be 'real' Python
726 objects for the current session, or text reprs from previous
727 sessions if db_log_output was enabled at the time. Where no output
728 is found, None is used.
730 Returns
731 -------
732 entries
733 An iterator over the desired lines. Each line is a 3-tuple, either
734 (session, line, input) if output is False, or
735 (session, line, (input, output)) if output is True.
736 """
737 if session <= 0:
738 session += self.session_number
739 if session==self.session_number: # Current session
740 return self._get_range_session(start, stop, raw, output)
741 return super(HistoryManager, self).get_range(session, start, stop, raw,
742 output)
744 ## ----------------------------
745 ## Methods for storing history:
746 ## ----------------------------
747 def store_inputs(self, line_num, source, source_raw=None):
748 """Store source and raw input in history and create input cache
749 variables ``_i*``.
751 Parameters
752 ----------
753 line_num : int
754 The prompt number of this input.
755 source : str
756 Python input.
757 source_raw : str, optional
758 If given, this is the raw input without any IPython transformations
759 applied to it. If not given, ``source`` is used.
760 """
761 if source_raw is None:
762 source_raw = source
763 source = source.rstrip('\n')
764 source_raw = source_raw.rstrip('\n')
766 # do not store exit/quit commands
767 if self._exit_re.match(source_raw.strip()):
768 return
770 self.input_hist_parsed.append(source)
771 self.input_hist_raw.append(source_raw)
773 with self.db_input_cache_lock:
774 self.db_input_cache.append((line_num, source, source_raw))
775 # Trigger to flush cache and write to DB.
776 if len(self.db_input_cache) >= self.db_cache_size:
777 self.save_flag.set()
779 # update the auto _i variables
780 self._iii = self._ii
781 self._ii = self._i
782 self._i = self._i00
783 self._i00 = source_raw
785 # hackish access to user namespace to create _i1,_i2... dynamically
786 new_i = '_i%s' % line_num
787 to_main = {'_i': self._i,
788 '_ii': self._ii,
789 '_iii': self._iii,
790 new_i : self._i00 }
792 if self.shell is not None:
793 self.shell.push(to_main, interactive=False)
795 def store_output(self, line_num):
796 """If database output logging is enabled, this saves all the
797 outputs from the indicated prompt number to the database. It's
798 called by run_cell after code has been executed.
800 Parameters
801 ----------
802 line_num : int
803 The line number from which to save outputs
804 """
805 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
806 return
807 output = self.output_hist_reprs[line_num]
809 with self.db_output_cache_lock:
810 self.db_output_cache.append((line_num, output))
811 if self.db_cache_size <= 1:
812 self.save_flag.set()
814 def _writeout_input_cache(self, conn):
815 with conn:
816 for line in self.db_input_cache:
817 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
818 (self.session_number,)+line)
820 def _writeout_output_cache(self, conn):
821 with conn:
822 for line in self.db_output_cache:
823 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
824 (self.session_number,)+line)
826 @only_when_enabled
827 def writeout_cache(self, conn=None):
828 """Write any entries in the cache to the database."""
829 if conn is None:
830 conn = self.db
832 with self.db_input_cache_lock:
833 try:
834 self._writeout_input_cache(conn)
835 except sqlite3.IntegrityError:
836 self.new_session(conn)
837 print("ERROR! Session/line number was not unique in",
838 "database. History logging moved to new session",
839 self.session_number)
840 try:
841 # Try writing to the new session. If this fails, don't
842 # recurse
843 self._writeout_input_cache(conn)
844 except sqlite3.IntegrityError:
845 pass
846 finally:
847 self.db_input_cache = []
849 with self.db_output_cache_lock:
850 try:
851 self._writeout_output_cache(conn)
852 except sqlite3.IntegrityError:
853 print("!! Session/line number for output was not unique",
854 "in database. Output will not be stored.")
855 finally:
856 self.db_output_cache = []
859class HistorySavingThread(threading.Thread):
860 """This thread takes care of writing history to the database, so that
861 the UI isn't held up while that happens.
863 It waits for the HistoryManager's save_flag to be set, then writes out
864 the history cache. The main thread is responsible for setting the flag when
865 the cache size reaches a defined threshold."""
866 daemon = True
867 stop_now = False
868 enabled = True
869 def __init__(self, history_manager):
870 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
871 self.history_manager = history_manager
872 self.enabled = history_manager.enabled
873 atexit.register(self.stop)
875 @only_when_enabled
876 def run(self):
877 # We need a separate db connection per thread:
878 try:
879 self.db = sqlite3.connect(
880 str(self.history_manager.hist_file),
881 **self.history_manager.connection_options,
882 )
883 while True:
884 self.history_manager.save_flag.wait()
885 if self.stop_now:
886 self.db.close()
887 return
888 self.history_manager.save_flag.clear()
889 self.history_manager.writeout_cache(self.db)
890 except Exception as e:
891 print(("The history saving thread hit an unexpected error (%s)."
892 "History will not be written to the database.") % repr(e))
894 def stop(self):
895 """This can be called from the main thread to safely stop this thread.
897 Note that it does not attempt to write out remaining history before
898 exiting. That should be done by calling the HistoryManager's
899 end_session method."""
900 self.stop_now = True
901 self.history_manager.save_flag.set()
902 self.join()
905# To match, e.g. ~5/8-~2/3
906range_re = re.compile(r"""
907((?P<startsess>~?\d+)/)?
908(?P<start>\d+)?
909((?P<sep>[\-:])
910 ((?P<endsess>~?\d+)/)?
911 (?P<end>\d+))?
912$""", re.VERBOSE)
915def extract_hist_ranges(ranges_str):
916 """Turn a string of history ranges into 3-tuples of (session, start, stop).
918 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
919 session".
921 Examples
922 --------
923 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
924 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
925 """
926 if ranges_str == "":
927 yield (0, 1, None) # Everything from current session
928 return
930 for range_str in ranges_str.split():
931 rmatch = range_re.match(range_str)
932 if not rmatch:
933 continue
934 start = rmatch.group("start")
935 if start:
936 start = int(start)
937 end = rmatch.group("end")
938 # If no end specified, get (a, a + 1)
939 end = int(end) if end else start + 1
940 else: # start not specified
941 if not rmatch.group('startsess'): # no startsess
942 continue
943 start = 1
944 end = None # provide the entire session hist
946 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
947 end += 1
948 startsess = rmatch.group("startsess") or "0"
949 endsess = rmatch.group("endsess") or startsess
950 startsess = int(startsess.replace("~","-"))
951 endsess = int(endsess.replace("~","-"))
952 assert endsess >= startsess, "start session must be earlier than end session"
954 if endsess == startsess:
955 yield (startsess, start, end)
956 continue
957 # Multiple sessions in one range:
958 yield (startsess, start, None)
959 for sess in range(startsess+1, endsess):
960 yield (sess, 1, None)
961 yield (endsess, 1, end)
964def _format_lineno(session, line):
965 """Helper function to format line numbers properly."""
966 if session == 0:
967 return str(line)
968 return "%s#%s" % (session, line)