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