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