##// END OF EJS Templates
deprecare remark and reformat history.py
M Bussonnier -
Show More
@@ -1,4 +1,4
1 """ History related magics and functionality """
1 """History related magics and functionality"""
2
2
3 from __future__ import annotations
3 from __future__ import annotations
4
4
@@ -34,6 +34,7 from IPython.paths import locate_profile
34 from IPython.utils.decorators import undoc
34 from IPython.utils.decorators import undoc
35 from typing import Iterable, Tuple, Optional, TYPE_CHECKING
35 from typing import Iterable, Tuple, Optional, TYPE_CHECKING
36 import typing
36 import typing
37 from warnings import warn
37
38
38 if TYPE_CHECKING:
39 if TYPE_CHECKING:
39 from IPython.core.interactiveshell import InteractiveShell
40 from IPython.core.interactiveshell import InteractiveShell
@@ -56,9 +57,10 except ModuleNotFoundError:
56
57
57 InOrInOut = typing.Union[str, Tuple[str, Optional[str]]]
58 InOrInOut = typing.Union[str, Tuple[str, Optional[str]]]
58
59
59 #-----------------------------------------------------------------------------
60 # -----------------------------------------------------------------------------
60 # Classes and functions
61 # Classes and functions
61 #-----------------------------------------------------------------------------
62 # -----------------------------------------------------------------------------
63
62
64
63 @undoc
65 @undoc
64 class DummyDB:
66 class DummyDB:
@@ -92,6 +94,7 def only_when_enabled(f, self, *a, **kw): # type: ignore [no-untyped-def]
92 # that should be at least 100 entries or so
94 # that should be at least 100 entries or so
93 _SAVE_DB_SIZE = 16384
95 _SAVE_DB_SIZE = 16384
94
96
97
95 @decorator
98 @decorator
96 def catch_corrupt_db(f, self, *a, **kw): # type: ignore [no-untyped-def]
99 def catch_corrupt_db(f, self, *a, **kw): # type: ignore [no-untyped-def]
97 """A decorator which wraps HistoryAccessor method calls to catch errors from
100 """A decorator which wraps HistoryAccessor method calls to catch errors from
@@ -106,10 +109,12 def catch_corrupt_db(f, self, *a, **kw): # type: ignore [no-untyped-def]
106 except (DatabaseError, OperationalError) as e:
109 except (DatabaseError, OperationalError) as e:
107 self._corrupt_db_counter += 1
110 self._corrupt_db_counter += 1
108 self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e)
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 if self._corrupt_db_counter > self._corrupt_db_limit:
113 if self._corrupt_db_counter > self._corrupt_db_limit:
111 self.hist_file = ':memory:'
114 self.hist_file = ":memory:"
112 self.log.error("Failed to load history too many times, history will not be saved.")
115 self.log.error(
116 "Failed to load history too many times, history will not be saved."
117 )
113 elif self.hist_file.is_file():
118 elif self.hist_file.is_file():
114 # move the file out of the way
119 # move the file out of the way
115 base = str(self.hist_file.parent / self.hist_file.stem)
120 base = str(self.hist_file.parent / self.hist_file.stem)
@@ -117,20 +122,22 def catch_corrupt_db(f, self, *a, **kw): # type: ignore [no-untyped-def]
117 size = self.hist_file.stat().st_size
122 size = self.hist_file.stat().st_size
118 if size >= _SAVE_DB_SIZE:
123 if size >= _SAVE_DB_SIZE:
119 # if there's significant content, avoid clobbering
124 # if there's significant content, avoid clobbering
120 now = datetime.datetime.now().isoformat().replace(':', '.')
125 now = datetime.datetime.now().isoformat().replace(":", ".")
121 newpath = base + '-corrupt-' + now + ext
126 newpath = base + "-corrupt-" + now + ext
122 # don't clobber previous corrupt backups
127 # don't clobber previous corrupt backups
123 for i in range(100):
128 for i in range(100):
124 if not Path(newpath).exists():
129 if not Path(newpath).exists():
125 break
130 break
126 else:
131 else:
127 newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext
132 newpath = base + "-corrupt-" + now + ("-%i" % i) + ext
128 else:
133 else:
129 # not much content, possibly empty; don't worry about clobbering
134 # not much content, possibly empty; don't worry about clobbering
130 # maybe we should just delete it?
135 # maybe we should just delete it?
131 newpath = base + '-corrupt' + ext
136 newpath = base + "-corrupt" + ext
132 self.hist_file.rename(newpath)
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 self.init_db()
141 self.init_db()
135 return []
142 return []
136 else:
143 else:
@@ -139,7 +146,7 def catch_corrupt_db(f, self, *a, **kw): # type: ignore [no-untyped-def]
139
146
140
147
141 class HistoryAccessorBase(LoggingConfigurable):
148 class HistoryAccessorBase(LoggingConfigurable):
142 """An abstract class for History Accessors """
149 """An abstract class for History Accessors"""
143
150
144 def get_tail(
151 def get_tail(
145 self,
152 self,
@@ -185,7 +192,7 class HistoryAccessor(HistoryAccessorBase):
185
192
186 # counter for init_db retries, so we don't keep trying over and over
193 # counter for init_db retries, so we don't keep trying over and over
187 _corrupt_db_counter = 0
194 _corrupt_db_counter = 0
188 # after two failures, fallback on :memory:
195 # after two failures, fallback on :memory:
189 _corrupt_db_limit = 2
196 _corrupt_db_limit = 2
190
197
191 # String holding the path to the history file
198 # String holding the path to the history file
@@ -234,15 +241,18 class HistoryAccessor(HistoryAccessorBase):
234
241
235 # The SQLite database
242 # The SQLite database
236 db = Any()
243 db = Any()
237 @observe('db')
244
245 @observe("db")
238 @only_when_enabled
246 @only_when_enabled
239 def _db_changed(self, change): # type: ignore [no-untyped-def]
247 def _db_changed(self, change): # type: ignore [no-untyped-def]
240 """validate the db, since it can be an Instance of two different types"""
248 """validate the db, since it can be an Instance of two different types"""
241 new = change['new']
249 new = change["new"]
242 connection_types = (DummyDB, sqlite3.Connection)
250 connection_types = (DummyDB, sqlite3.Connection)
243 if not isinstance(new, connection_types):
251 if not isinstance(new, connection_types):
244 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
252 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % (
245 (self.__class__.__name__, new)
253 self.__class__.__name__,
254 new,
255 )
246 raise TraitError(msg)
256 raise TraitError(msg)
247
257
248 def __init__(
258 def __init__(
@@ -296,7 +306,7 class HistoryAccessor(HistoryAccessorBase):
296 return
306 return
297
307
298 # use detect_types so that timestamps return datetime objects
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 kwargs.update(self.connection_options)
310 kwargs.update(self.connection_options)
301 self.db = sqlite3.connect(str(self.hist_file), **kwargs) # type: ignore [call-overload]
311 self.db = sqlite3.connect(str(self.hist_file), **kwargs) # type: ignore [call-overload]
302 with self.db:
312 with self.db:
@@ -353,7 +363,7 class HistoryAccessor(HistoryAccessorBase):
353 -------
363 -------
354 Tuples as :meth:`get_range`
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 sqlfrom = "history"
367 sqlfrom = "history"
358 if output:
368 if output:
359 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
369 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
@@ -364,7 +374,7 class HistoryAccessor(HistoryAccessorBase):
364 cur = self.db.execute(this_querry, params)
374 cur = self.db.execute(this_querry, params)
365 if latest:
375 if latest:
366 cur = (row[:-1] for row in cur)
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 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
378 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
369 return cur
379 return cur
370
380
@@ -390,7 +400,7 class HistoryAccessor(HistoryAccessorBase):
390 Timestamp for the end of the session, or None if IPython crashed.
400 Timestamp for the end of the session, or None if IPython crashed.
391 num_cmds : int
401 num_cmds : int
392 Number of commands run, or None if IPython crashed.
402 Number of commands run, or None if IPython crashed.
393 remark : unicode
403 remark : str
394 A manually set description.
404 A manually set description.
395 """
405 """
396 query = "SELECT * from sessions where session == ?"
406 query = "SELECT * from sessions where session == ?"
@@ -480,7 +490,7 class HistoryAccessor(HistoryAccessorBase):
480 sqlform = "WHERE %s GLOB ?" % tosearch
490 sqlform = "WHERE %s GLOB ?" % tosearch
481 params: typing.Tuple[typing.Any, ...] = (pattern,)
491 params: typing.Tuple[typing.Any, ...] = (pattern,)
482 if unique:
492 if unique:
483 sqlform += ' GROUP BY {0}'.format(tosearch)
493 sqlform += " GROUP BY {0}".format(tosearch)
484 if n is not None:
494 if n is not None:
485 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
495 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
486 params += (n,)
496 params += (n,)
@@ -534,8 +544,9 class HistoryAccessor(HistoryAccessorBase):
534 lineclause = "line>=?"
544 lineclause = "line>=?"
535 params = (session, start)
545 params = (session, start)
536
546
537 return self._run_sql("WHERE session==? AND %s" % lineclause,
547 return self._run_sql(
538 params, raw=raw, output=output)
548 "WHERE session==? AND %s" % lineclause, params, raw=raw, output=output
549 )
539
550
540 def get_range_by_str(
551 def get_range_by_str(
541 self, rangestr: str, raw: bool = True, output: bool = False
552 self, rangestr: str, raw: bool = True, output: bool = False
@@ -564,8 +575,8 class HistoryAccessor(HistoryAccessorBase):
564
575
565
576
566 class HistoryManager(HistoryAccessor):
577 class HistoryManager(HistoryAccessor):
567 """A class to organize all history-related functionality in one place.
578 """A class to organize all history-related functionality in one place."""
568 """
579
569 # Public interface
580 # Public interface
570
581
571 # An instance of the IPython shell we are attached to
582 # An instance of the IPython shell we are attached to
@@ -595,20 +606,20 class HistoryManager(HistoryAccessor):
595 # The number of the current session in the history database
606 # The number of the current session in the history database
596 session_number: int = Integer() # type: ignore [assignment]
607 session_number: int = Integer() # type: ignore [assignment]
597
608
598 db_log_output = Bool(False,
609 db_log_output = Bool(
599 help="Should the history database include output? (default: no)"
610 False, help="Should the history database include output? (default: no)"
600 ).tag(config=True)
611 ).tag(config=True)
601 db_cache_size = Integer(0,
612 db_cache_size = Integer(
613 0,
602 help="Write to database every x commands (higher values save disk access & power).\n"
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 ).tag(config=True)
616 ).tag(config=True)
605 # The input and output caches
617 # The input and output caches
606 db_input_cache: List[Tuple[int, str, str]] = List()
618 db_input_cache: List[Tuple[int, str, str]] = List()
607 db_output_cache: List[Tuple[int, str]] = List()
619 db_output_cache: List[Tuple[int, str]] = List()
608
620
609 # History saving in separate thread
621 # History saving in separate thread
610 save_thread = Instance('IPython.core.history.HistorySavingThread',
622 save_thread = Instance("IPython.core.history.HistorySavingThread", allow_none=True)
611 allow_none=True)
612 save_flag = Instance(threading.Event, allow_none=False)
623 save_flag = Instance(threading.Event, allow_none=False)
613
624
614 # Private interface
625 # Private interface
@@ -640,11 +651,14 class HistoryManager(HistoryAccessor):
640 try:
651 try:
641 self.new_session()
652 self.new_session()
642 except OperationalError:
653 except OperationalError:
643 self.log.error("Failed to create history session in %s. History will not be saved.",
654 self.log.error(
644 self.hist_file, exc_info=True)
655 "Failed to create history session in %s. History will not be saved.",
645 self.hist_file = ':memory:'
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 self.save_thread = HistorySavingThread(self)
662 self.save_thread = HistorySavingThread(self)
649 try:
663 try:
650 self.save_thread.start()
664 self.save_thread.start()
@@ -695,9 +709,16 class HistoryManager(HistoryAccessor):
695
709
696 def name_session(self, name: str) -> None:
710 def name_session(self, name: str) -> None:
697 """Give the current session a name in the history database."""
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 with self.db:
717 with self.db:
699 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
718 self.db.execute(
700 (name, self.session_number))
719 "UPDATE sessions SET remark=? WHERE session==?",
720 (name, self.session_number),
721 )
701
722
702 def reset(self, new_session: bool = True) -> None:
723 def reset(self, new_session: bool = True) -> None:
703 """Clear the session history, releasing all object references, and
724 """Clear the session history, releasing all object references, and
@@ -737,7 +758,7 class HistoryManager(HistoryAccessor):
737 Timestamp for the end of the session, or None if IPython crashed.
758 Timestamp for the end of the session, or None if IPython crashed.
738 num_cmds : int
759 num_cmds : int
739 Number of commands run, or None if IPython crashed.
760 Number of commands run, or None if IPython crashed.
740 remark : unicode
761 remark : str
741 A manually set description.
762 A manually set description.
742 """
763 """
743 if session <= 0:
764 if session <= 0:
@@ -867,10 +888,9 class HistoryManager(HistoryAccessor):
867 """
888 """
868 if session <= 0:
889 if session <= 0:
869 session += self.session_number
890 session += self.session_number
870 if session==self.session_number: # Current session
891 if session == self.session_number: # Current session
871 return self._get_range_session(start, stop, raw, output)
892 return self._get_range_session(start, stop, raw, output)
872 return super(HistoryManager, self).get_range(session, start, stop, raw,
893 return super(HistoryManager, self).get_range(session, start, stop, raw, output)
873 output)
874
894
875 ## ----------------------------
895 ## ----------------------------
876 ## Methods for storing history:
896 ## Methods for storing history:
@@ -893,8 +913,8 class HistoryManager(HistoryAccessor):
893 """
913 """
894 if source_raw is None:
914 if source_raw is None:
895 source_raw = source
915 source_raw = source
896 source = source.rstrip('\n')
916 source = source.rstrip("\n")
897 source_raw = source_raw.rstrip('\n')
917 source_raw = source_raw.rstrip("\n")
898
918
899 # do not store exit/quit commands
919 # do not store exit/quit commands
900 if self._exit_re.match(source_raw.strip()):
920 if self._exit_re.match(source_raw.strip()):
@@ -916,11 +936,8 class HistoryManager(HistoryAccessor):
916 self._i00 = source_raw
936 self._i00 = source_raw
917
937
918 # hackish access to user namespace to create _i1,_i2... dynamically
938 # hackish access to user namespace to create _i1,_i2... dynamically
919 new_i = '_i%s' % line_num
939 new_i = "_i%s" % line_num
920 to_main = {'_i': self._i,
940 to_main = {"_i": self._i, "_ii": self._ii, "_iii": self._iii, new_i: self._i00}
921 '_ii': self._ii,
922 '_iii': self._iii,
923 new_i : self._i00 }
924
941
925 if self.shell is not None:
942 if self.shell is not None:
926 self.shell.push(to_main, interactive=False)
943 self.shell.push(to_main, interactive=False)
@@ -947,14 +964,18 class HistoryManager(HistoryAccessor):
947 def _writeout_input_cache(self, conn: sqlite3.Connection) -> None:
964 def _writeout_input_cache(self, conn: sqlite3.Connection) -> None:
948 with conn:
965 with conn:
949 for line in self.db_input_cache:
966 for line in self.db_input_cache:
950 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
967 conn.execute(
951 (self.session_number,)+line)
968 "INSERT INTO history VALUES (?, ?, ?, ?)",
969 (self.session_number,) + line,
970 )
952
971
953 def _writeout_output_cache(self, conn: sqlite3.Connection) -> None:
972 def _writeout_output_cache(self, conn: sqlite3.Connection) -> None:
954 with conn:
973 with conn:
955 for line in self.db_output_cache:
974 for line in self.db_output_cache:
956 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
975 conn.execute(
957 (self.session_number,)+line)
976 "INSERT INTO output_history VALUES (?, ?, ?)",
977 (self.session_number,) + line,
978 )
958
979
959 @only_when_enabled
980 @only_when_enabled
960 def writeout_cache(self, conn: Optional[sqlite3.Connection] = None) -> None:
981 def writeout_cache(self, conn: Optional[sqlite3.Connection] = None) -> None:
@@ -967,9 +988,11 class HistoryManager(HistoryAccessor):
967 self._writeout_input_cache(conn)
988 self._writeout_input_cache(conn)
968 except sqlite3.IntegrityError:
989 except sqlite3.IntegrityError:
969 self.new_session(conn)
990 self.new_session(conn)
970 print("ERROR! Session/line number was not unique in",
991 print(
971 "database. History logging moved to new session",
992 "ERROR! Session/line number was not unique in",
972 self.session_number)
993 "database. History logging moved to new session",
994 self.session_number,
995 )
973 try:
996 try:
974 # Try writing to the new session. If this fails, don't
997 # Try writing to the new session. If this fails, don't
975 # recurse
998 # recurse
@@ -983,8 +1006,10 class HistoryManager(HistoryAccessor):
983 try:
1006 try:
984 self._writeout_output_cache(conn)
1007 self._writeout_output_cache(conn)
985 except sqlite3.IntegrityError:
1008 except sqlite3.IntegrityError:
986 print("!! Session/line number for output was not unique",
1009 print(
987 "in database. Output will not be stored.")
1010 "!! Session/line number for output was not unique",
1011 "in database. Output will not be stored.",
1012 )
988 finally:
1013 finally:
989 self.db_output_cache = []
1014 self.db_output_cache = []
990
1015
@@ -996,6 +1021,7 class HistorySavingThread(threading.Thread):
996 It waits for the HistoryManager's save_flag to be set, then writes out
1021 It waits for the HistoryManager's save_flag to be set, then writes out
997 the history cache. The main thread is responsible for setting the flag when
1022 the history cache. The main thread is responsible for setting the flag when
998 the cache size reaches a defined threshold."""
1023 the cache size reaches a defined threshold."""
1024
999 daemon = True
1025 daemon = True
1000 stop_now = False
1026 stop_now = False
1001 enabled = True
1027 enabled = True
@@ -1023,8 +1049,13 class HistorySavingThread(threading.Thread):
1023 self.history_manager.save_flag.clear()
1049 self.history_manager.save_flag.clear()
1024 self.history_manager.writeout_cache(self.db)
1050 self.history_manager.writeout_cache(self.db)
1025 except Exception as e:
1051 except Exception as e:
1026 print(("The history saving thread hit an unexpected error (%s)."
1052 print(
1027 "History will not be written to the database.") % repr(e))
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 finally:
1059 finally:
1029 atexit.unregister(self.stop)
1060 atexit.unregister(self.stop)
1030
1061
@@ -1040,13 +1071,16 class HistorySavingThread(threading.Thread):
1040
1071
1041
1072
1042 # To match, e.g. ~5/8-~2/3
1073 # To match, e.g. ~5/8-~2/3
1043 range_re = re.compile(r"""
1074 range_re = re.compile(
1075 r"""
1044 ((?P<startsess>~?\d+)/)?
1076 ((?P<startsess>~?\d+)/)?
1045 (?P<start>\d+)?
1077 (?P<start>\d+)?
1046 ((?P<sep>[\-:])
1078 ((?P<sep>[\-:])
1047 ((?P<endsess>~?\d+)/)?
1079 ((?P<endsess>~?\d+)/)?
1048 (?P<end>\d+))?
1080 (?P<end>\d+))?
1049 $""", re.VERBOSE)
1081 $""",
1082 re.VERBOSE,
1083 )
1050
1084
1051
1085
1052 def extract_hist_ranges(ranges_str: str) -> Iterable[Tuple[int, int, Optional[int]]]:
1086 def extract_hist_ranges(ranges_str: str) -> Iterable[Tuple[int, int, Optional[int]]]:
@@ -1075,18 +1109,18 def extract_hist_ranges(ranges_str: str) -> Iterable[Tuple[int, int, Optional[in
1075 # If no end specified, get (a, a + 1)
1109 # If no end specified, get (a, a + 1)
1076 end = int(end) if end else start + 1
1110 end = int(end) if end else start + 1
1077 else: # start not specified
1111 else: # start not specified
1078 if not rmatch.group('startsess'): # no startsess
1112 if not rmatch.group("startsess"): # no startsess
1079 continue
1113 continue
1080 start = 1
1114 start = 1
1081 end = None # provide the entire session hist
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 assert end is not None
1118 assert end is not None
1085 end += 1
1119 end += 1
1086 startsess = rmatch.group("startsess") or "0"
1120 startsess = rmatch.group("startsess") or "0"
1087 endsess = rmatch.group("endsess") or startsess
1121 endsess = rmatch.group("endsess") or startsess
1088 startsess = int(startsess.replace("~","-"))
1122 startsess = int(startsess.replace("~", "-"))
1089 endsess = int(endsess.replace("~","-"))
1123 endsess = int(endsess.replace("~", "-"))
1090 assert endsess >= startsess, "start session must be earlier than end session"
1124 assert endsess >= startsess, "start session must be earlier than end session"
1091
1125
1092 if endsess == startsess:
1126 if endsess == startsess:
@@ -1094,7 +1128,7 def extract_hist_ranges(ranges_str: str) -> Iterable[Tuple[int, int, Optional[in
1094 continue
1128 continue
1095 # Multiple sessions in one range:
1129 # Multiple sessions in one range:
1096 yield (startsess, start, None)
1130 yield (startsess, start, None)
1097 for sess in range(startsess+1, endsess):
1131 for sess in range(startsess + 1, endsess):
1098 yield (sess, 1, None)
1132 yield (sess, 1, None)
1099 yield (endsess, 1, end)
1133 yield (endsess, 1, end)
1100
1134
General Comments 0
You need to be logged in to leave comments. Login now