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