##// END OF EJS Templates
Merge pull request #13406 from Carreau/atomic-connect...
Matthias Bussonnier -
r27341:28eaf257 merge
parent child Browse files
Show More
@@ -1,907 +1,913 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 self.db.execute("""CREATE TABLE IF NOT EXISTS sessions (session integer
245 with self.db:
246 primary key autoincrement, start timestamp,
246 self.db.execute(
247 end timestamp, num_cmds integer, remark text)""")
247 """CREATE TABLE IF NOT EXISTS sessions (session integer
248 self.db.execute("""CREATE TABLE IF NOT EXISTS history
248 primary key autoincrement, start timestamp,
249 (session integer, line integer, source text, source_raw text,
249 end timestamp, num_cmds integer, remark text)"""
250 PRIMARY KEY (session, line))""")
250 )
251 # Output history is optional, but ensure the table's there so it can be
251 self.db.execute(
252 # enabled later.
252 """CREATE TABLE IF NOT EXISTS history
253 self.db.execute("""CREATE TABLE IF NOT EXISTS output_history
253 (session integer, line integer, source text, source_raw text,
254 (session integer, line integer, output text,
254 PRIMARY KEY (session, line))"""
255 PRIMARY KEY (session, line))""")
255 )
256 self.db.commit()
256 # Output history is optional, but ensure the table's there so it can be
257 # enabled later.
258 self.db.execute(
259 """CREATE TABLE IF NOT EXISTS output_history
260 (session integer, line integer, output text,
261 PRIMARY KEY (session, line))"""
262 )
257 # success! reset corrupt db count
263 # success! reset corrupt db count
258 self._corrupt_db_counter = 0
264 self._corrupt_db_counter = 0
259
265
260 def writeout_cache(self):
266 def writeout_cache(self):
261 """Overridden by HistoryManager to dump the cache before certain
267 """Overridden by HistoryManager to dump the cache before certain
262 database lookups."""
268 database lookups."""
263 pass
269 pass
264
270
265 ## -------------------------------
271 ## -------------------------------
266 ## Methods for retrieving history:
272 ## Methods for retrieving history:
267 ## -------------------------------
273 ## -------------------------------
268 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):
269 """Prepares and runs an SQL query for the history database.
275 """Prepares and runs an SQL query for the history database.
270
276
271 Parameters
277 Parameters
272 ----------
278 ----------
273 sql : str
279 sql : str
274 Any filtering expressions to go after SELECT ... FROM ...
280 Any filtering expressions to go after SELECT ... FROM ...
275 params : tuple
281 params : tuple
276 Parameters passed to the SQL query (to replace "?")
282 Parameters passed to the SQL query (to replace "?")
277 raw, output : bool
283 raw, output : bool
278 See :meth:`get_range`
284 See :meth:`get_range`
279 latest : bool
285 latest : bool
280 Select rows with max (session, line)
286 Select rows with max (session, line)
281
287
282 Returns
288 Returns
283 -------
289 -------
284 Tuples as :meth:`get_range`
290 Tuples as :meth:`get_range`
285 """
291 """
286 toget = 'source_raw' if raw else 'source'
292 toget = 'source_raw' if raw else 'source'
287 sqlfrom = "history"
293 sqlfrom = "history"
288 if output:
294 if output:
289 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
295 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
290 toget = "history.%s, output_history.output" % toget
296 toget = "history.%s, output_history.output" % toget
291 if latest:
297 if latest:
292 toget += ", MAX(session * 128 * 1024 + line)"
298 toget += ", MAX(session * 128 * 1024 + line)"
293 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
299 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
294 (toget, sqlfrom) + sql, params)
300 (toget, sqlfrom) + sql, params)
295 if latest:
301 if latest:
296 cur = (row[:-1] for row in cur)
302 cur = (row[:-1] for row in cur)
297 if output: # Regroup into 3-tuples, and parse JSON
303 if output: # Regroup into 3-tuples, and parse JSON
298 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)
299 return cur
305 return cur
300
306
301 @only_when_enabled
307 @only_when_enabled
302 @catch_corrupt_db
308 @catch_corrupt_db
303 def get_session_info(self, session):
309 def get_session_info(self, session):
304 """Get info about a session.
310 """Get info about a session.
305
311
306 Parameters
312 Parameters
307 ----------
313 ----------
308 session : int
314 session : int
309 Session number to retrieve.
315 Session number to retrieve.
310
316
311 Returns
317 Returns
312 -------
318 -------
313 session_id : int
319 session_id : int
314 Session ID number
320 Session ID number
315 start : datetime
321 start : datetime
316 Timestamp for the start of the session.
322 Timestamp for the start of the session.
317 end : datetime
323 end : datetime
318 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.
319 num_cmds : int
325 num_cmds : int
320 Number of commands run, or None if IPython crashed.
326 Number of commands run, or None if IPython crashed.
321 remark : unicode
327 remark : unicode
322 A manually set description.
328 A manually set description.
323 """
329 """
324 query = "SELECT * from sessions where session == ?"
330 query = "SELECT * from sessions where session == ?"
325 return self.db.execute(query, (session,)).fetchone()
331 return self.db.execute(query, (session,)).fetchone()
326
332
327 @catch_corrupt_db
333 @catch_corrupt_db
328 def get_last_session_id(self):
334 def get_last_session_id(self):
329 """Get the last session ID currently in the database.
335 """Get the last session ID currently in the database.
330
336
331 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
332 :attr:`HistoryManager.session_number`.
338 :attr:`HistoryManager.session_number`.
333 """
339 """
334 for record in self.get_tail(n=1, include_latest=True):
340 for record in self.get_tail(n=1, include_latest=True):
335 return record[0]
341 return record[0]
336
342
337 @catch_corrupt_db
343 @catch_corrupt_db
338 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):
339 """Get the last n lines from the history database.
345 """Get the last n lines from the history database.
340
346
341 Parameters
347 Parameters
342 ----------
348 ----------
343 n : int
349 n : int
344 The number of lines to get
350 The number of lines to get
345 raw, output : bool
351 raw, output : bool
346 See :meth:`get_range`
352 See :meth:`get_range`
347 include_latest : bool
353 include_latest : bool
348 If False (default), n+1 lines are fetched, and the latest one
354 If False (default), n+1 lines are fetched, and the latest one
349 is discarded. This is intended to be used where the function
355 is discarded. This is intended to be used where the function
350 is called by a user command, which it should not return.
356 is called by a user command, which it should not return.
351
357
352 Returns
358 Returns
353 -------
359 -------
354 Tuples as :meth:`get_range`
360 Tuples as :meth:`get_range`
355 """
361 """
356 self.writeout_cache()
362 self.writeout_cache()
357 if not include_latest:
363 if not include_latest:
358 n += 1
364 n += 1
359 cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
365 cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
360 (n,), raw=raw, output=output)
366 (n,), raw=raw, output=output)
361 if not include_latest:
367 if not include_latest:
362 return reversed(list(cur)[1:])
368 return reversed(list(cur)[1:])
363 return reversed(list(cur))
369 return reversed(list(cur))
364
370
365 @catch_corrupt_db
371 @catch_corrupt_db
366 def search(self, pattern="*", raw=True, search_raw=True,
372 def search(self, pattern="*", raw=True, search_raw=True,
367 output=False, n=None, unique=False):
373 output=False, n=None, unique=False):
368 """Search the database using unix glob-style matching (wildcards
374 """Search the database using unix glob-style matching (wildcards
369 * and ?).
375 * and ?).
370
376
371 Parameters
377 Parameters
372 ----------
378 ----------
373 pattern : str
379 pattern : str
374 The wildcarded pattern to match when searching
380 The wildcarded pattern to match when searching
375 search_raw : bool
381 search_raw : bool
376 If True, search the raw input, otherwise, the parsed input
382 If True, search the raw input, otherwise, the parsed input
377 raw, output : bool
383 raw, output : bool
378 See :meth:`get_range`
384 See :meth:`get_range`
379 n : None or int
385 n : None or int
380 If an integer is given, it defines the limit of
386 If an integer is given, it defines the limit of
381 returned entries.
387 returned entries.
382 unique : bool
388 unique : bool
383 When it is true, return only unique entries.
389 When it is true, return only unique entries.
384
390
385 Returns
391 Returns
386 -------
392 -------
387 Tuples as :meth:`get_range`
393 Tuples as :meth:`get_range`
388 """
394 """
389 tosearch = "source_raw" if search_raw else "source"
395 tosearch = "source_raw" if search_raw else "source"
390 if output:
396 if output:
391 tosearch = "history." + tosearch
397 tosearch = "history." + tosearch
392 self.writeout_cache()
398 self.writeout_cache()
393 sqlform = "WHERE %s GLOB ?" % tosearch
399 sqlform = "WHERE %s GLOB ?" % tosearch
394 params = (pattern,)
400 params = (pattern,)
395 if unique:
401 if unique:
396 sqlform += ' GROUP BY {0}'.format(tosearch)
402 sqlform += ' GROUP BY {0}'.format(tosearch)
397 if n is not None:
403 if n is not None:
398 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
404 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
399 params += (n,)
405 params += (n,)
400 elif unique:
406 elif unique:
401 sqlform += " ORDER BY session, line"
407 sqlform += " ORDER BY session, line"
402 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique)
408 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique)
403 if n is not None:
409 if n is not None:
404 return reversed(list(cur))
410 return reversed(list(cur))
405 return cur
411 return cur
406
412
407 @catch_corrupt_db
413 @catch_corrupt_db
408 def get_range(self, session, start=1, stop=None, raw=True,output=False):
414 def get_range(self, session, start=1, stop=None, raw=True,output=False):
409 """Retrieve input by session.
415 """Retrieve input by session.
410
416
411 Parameters
417 Parameters
412 ----------
418 ----------
413 session : int
419 session : int
414 Session number to retrieve.
420 Session number to retrieve.
415 start : int
421 start : int
416 First line to retrieve.
422 First line to retrieve.
417 stop : int
423 stop : int
418 End of line range (excluded from output itself). If None, retrieve
424 End of line range (excluded from output itself). If None, retrieve
419 to the end of the session.
425 to the end of the session.
420 raw : bool
426 raw : bool
421 If True, return untranslated input
427 If True, return untranslated input
422 output : bool
428 output : bool
423 If True, attempt to include output. This will be 'real' Python
429 If True, attempt to include output. This will be 'real' Python
424 objects for the current session, or text reprs from previous
430 objects for the current session, or text reprs from previous
425 sessions if db_log_output was enabled at the time. Where no output
431 sessions if db_log_output was enabled at the time. Where no output
426 is found, None is used.
432 is found, None is used.
427
433
428 Returns
434 Returns
429 -------
435 -------
430 entries
436 entries
431 An iterator over the desired lines. Each line is a 3-tuple, either
437 An iterator over the desired lines. Each line is a 3-tuple, either
432 (session, line, input) if output is False, or
438 (session, line, input) if output is False, or
433 (session, line, (input, output)) if output is True.
439 (session, line, (input, output)) if output is True.
434 """
440 """
435 if stop:
441 if stop:
436 lineclause = "line >= ? AND line < ?"
442 lineclause = "line >= ? AND line < ?"
437 params = (session, start, stop)
443 params = (session, start, stop)
438 else:
444 else:
439 lineclause = "line>=?"
445 lineclause = "line>=?"
440 params = (session, start)
446 params = (session, start)
441
447
442 return self._run_sql("WHERE session==? AND %s" % lineclause,
448 return self._run_sql("WHERE session==? AND %s" % lineclause,
443 params, raw=raw, output=output)
449 params, raw=raw, output=output)
444
450
445 def get_range_by_str(self, rangestr, raw=True, output=False):
451 def get_range_by_str(self, rangestr, raw=True, output=False):
446 """Get lines of history from a string of ranges, as used by magic
452 """Get lines of history from a string of ranges, as used by magic
447 commands %hist, %save, %macro, etc.
453 commands %hist, %save, %macro, etc.
448
454
449 Parameters
455 Parameters
450 ----------
456 ----------
451 rangestr : str
457 rangestr : str
452 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used,
458 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used,
453 this will return everything from current session's history.
459 this will return everything from current session's history.
454
460
455 See the documentation of :func:`%history` for the full details.
461 See the documentation of :func:`%history` for the full details.
456
462
457 raw, output : bool
463 raw, output : bool
458 As :meth:`get_range`
464 As :meth:`get_range`
459
465
460 Returns
466 Returns
461 -------
467 -------
462 Tuples as :meth:`get_range`
468 Tuples as :meth:`get_range`
463 """
469 """
464 for sess, s, e in extract_hist_ranges(rangestr):
470 for sess, s, e in extract_hist_ranges(rangestr):
465 for line in self.get_range(sess, s, e, raw=raw, output=output):
471 for line in self.get_range(sess, s, e, raw=raw, output=output):
466 yield line
472 yield line
467
473
468
474
469 class HistoryManager(HistoryAccessor):
475 class HistoryManager(HistoryAccessor):
470 """A class to organize all history-related functionality in one place.
476 """A class to organize all history-related functionality in one place.
471 """
477 """
472 # Public interface
478 # Public interface
473
479
474 # An instance of the IPython shell we are attached to
480 # An instance of the IPython shell we are attached to
475 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
481 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
476 allow_none=True)
482 allow_none=True)
477 # Lists to hold processed and raw history. These start with a blank entry
483 # Lists to hold processed and raw history. These start with a blank entry
478 # so that we can index them starting from 1
484 # so that we can index them starting from 1
479 input_hist_parsed = List([""])
485 input_hist_parsed = List([""])
480 input_hist_raw = List([""])
486 input_hist_raw = List([""])
481 # A list of directories visited during session
487 # A list of directories visited during session
482 dir_hist = List()
488 dir_hist = List()
483 @default('dir_hist')
489 @default('dir_hist')
484 def _dir_hist_default(self):
490 def _dir_hist_default(self):
485 try:
491 try:
486 return [Path.cwd()]
492 return [Path.cwd()]
487 except OSError:
493 except OSError:
488 return []
494 return []
489
495
490 # A dict of output history, keyed with ints from the shell's
496 # A dict of output history, keyed with ints from the shell's
491 # execution count.
497 # execution count.
492 output_hist = Dict()
498 output_hist = Dict()
493 # The text/plain repr of outputs.
499 # The text/plain repr of outputs.
494 output_hist_reprs = Dict()
500 output_hist_reprs = Dict()
495
501
496 # The number of the current session in the history database
502 # The number of the current session in the history database
497 session_number = Integer()
503 session_number = Integer()
498
504
499 db_log_output = Bool(False,
505 db_log_output = Bool(False,
500 help="Should the history database include output? (default: no)"
506 help="Should the history database include output? (default: no)"
501 ).tag(config=True)
507 ).tag(config=True)
502 db_cache_size = Integer(0,
508 db_cache_size = Integer(0,
503 help="Write to database every x commands (higher values save disk access & power).\n"
509 help="Write to database every x commands (higher values save disk access & power).\n"
504 "Values of 1 or less effectively disable caching."
510 "Values of 1 or less effectively disable caching."
505 ).tag(config=True)
511 ).tag(config=True)
506 # The input and output caches
512 # The input and output caches
507 db_input_cache = List()
513 db_input_cache = List()
508 db_output_cache = List()
514 db_output_cache = List()
509
515
510 # History saving in separate thread
516 # History saving in separate thread
511 save_thread = Instance('IPython.core.history.HistorySavingThread',
517 save_thread = Instance('IPython.core.history.HistorySavingThread',
512 allow_none=True)
518 allow_none=True)
513 save_flag = Instance(threading.Event, allow_none=True)
519 save_flag = Instance(threading.Event, allow_none=True)
514
520
515 # Private interface
521 # Private interface
516 # Variables used to store the three last inputs from the user. On each new
522 # Variables used to store the three last inputs from the user. On each new
517 # history update, we populate the user's namespace with these, shifted as
523 # history update, we populate the user's namespace with these, shifted as
518 # necessary.
524 # necessary.
519 _i00 = Unicode(u'')
525 _i00 = Unicode(u'')
520 _i = Unicode(u'')
526 _i = Unicode(u'')
521 _ii = Unicode(u'')
527 _ii = Unicode(u'')
522 _iii = Unicode(u'')
528 _iii = Unicode(u'')
523
529
524 # A regex matching all forms of the exit command, so that we don't store
530 # A regex matching all forms of the exit command, so that we don't store
525 # them in the history (it's annoying to rewind the first entry and land on
531 # them in the history (it's annoying to rewind the first entry and land on
526 # an exit call).
532 # an exit call).
527 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
533 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
528
534
529 def __init__(self, shell=None, config=None, **traits):
535 def __init__(self, shell=None, config=None, **traits):
530 """Create a new history manager associated with a shell instance.
536 """Create a new history manager associated with a shell instance.
531 """
537 """
532 # We need a pointer back to the shell for various tasks.
538 # We need a pointer back to the shell for various tasks.
533 super(HistoryManager, self).__init__(shell=shell, config=config,
539 super(HistoryManager, self).__init__(shell=shell, config=config,
534 **traits)
540 **traits)
535 self.save_flag = threading.Event()
541 self.save_flag = threading.Event()
536 self.db_input_cache_lock = threading.Lock()
542 self.db_input_cache_lock = threading.Lock()
537 self.db_output_cache_lock = threading.Lock()
543 self.db_output_cache_lock = threading.Lock()
538
544
539 try:
545 try:
540 self.new_session()
546 self.new_session()
541 except sqlite3.OperationalError:
547 except sqlite3.OperationalError:
542 self.log.error("Failed to create history session in %s. History will not be saved.",
548 self.log.error("Failed to create history session in %s. History will not be saved.",
543 self.hist_file, exc_info=True)
549 self.hist_file, exc_info=True)
544 self.hist_file = ':memory:'
550 self.hist_file = ':memory:'
545
551
546 if self.enabled and self.hist_file != ':memory:':
552 if self.enabled and self.hist_file != ':memory:':
547 self.save_thread = HistorySavingThread(self)
553 self.save_thread = HistorySavingThread(self)
548 self.save_thread.start()
554 self.save_thread.start()
549
555
550 def _get_hist_file_name(self, profile=None):
556 def _get_hist_file_name(self, profile=None):
551 """Get default history file name based on the Shell's profile.
557 """Get default history file name based on the Shell's profile.
552
558
553 The profile parameter is ignored, but must exist for compatibility with
559 The profile parameter is ignored, but must exist for compatibility with
554 the parent class."""
560 the parent class."""
555 profile_dir = self.shell.profile_dir.location
561 profile_dir = self.shell.profile_dir.location
556 return Path(profile_dir) / "history.sqlite"
562 return Path(profile_dir) / "history.sqlite"
557
563
558 @only_when_enabled
564 @only_when_enabled
559 def new_session(self, conn=None):
565 def new_session(self, conn=None):
560 """Get a new session number."""
566 """Get a new session number."""
561 if conn is None:
567 if conn is None:
562 conn = self.db
568 conn = self.db
563
569
564 with conn:
570 with conn:
565 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
571 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
566 NULL, "") """, (datetime.datetime.now(),))
572 NULL, "") """, (datetime.datetime.now(),))
567 self.session_number = cur.lastrowid
573 self.session_number = cur.lastrowid
568
574
569 def end_session(self):
575 def end_session(self):
570 """Close the database session, filling in the end time and line count."""
576 """Close the database session, filling in the end time and line count."""
571 self.writeout_cache()
577 self.writeout_cache()
572 with self.db:
578 with self.db:
573 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
579 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
574 session==?""", (datetime.datetime.now(),
580 session==?""", (datetime.datetime.now(),
575 len(self.input_hist_parsed)-1, self.session_number))
581 len(self.input_hist_parsed)-1, self.session_number))
576 self.session_number = 0
582 self.session_number = 0
577
583
578 def name_session(self, name):
584 def name_session(self, name):
579 """Give the current session a name in the history database."""
585 """Give the current session a name in the history database."""
580 with self.db:
586 with self.db:
581 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
587 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
582 (name, self.session_number))
588 (name, self.session_number))
583
589
584 def reset(self, new_session=True):
590 def reset(self, new_session=True):
585 """Clear the session history, releasing all object references, and
591 """Clear the session history, releasing all object references, and
586 optionally open a new session."""
592 optionally open a new session."""
587 self.output_hist.clear()
593 self.output_hist.clear()
588 # The directory history can't be completely empty
594 # The directory history can't be completely empty
589 self.dir_hist[:] = [Path.cwd()]
595 self.dir_hist[:] = [Path.cwd()]
590
596
591 if new_session:
597 if new_session:
592 if self.session_number:
598 if self.session_number:
593 self.end_session()
599 self.end_session()
594 self.input_hist_parsed[:] = [""]
600 self.input_hist_parsed[:] = [""]
595 self.input_hist_raw[:] = [""]
601 self.input_hist_raw[:] = [""]
596 self.new_session()
602 self.new_session()
597
603
598 # ------------------------------
604 # ------------------------------
599 # Methods for retrieving history
605 # Methods for retrieving history
600 # ------------------------------
606 # ------------------------------
601 def get_session_info(self, session=0):
607 def get_session_info(self, session=0):
602 """Get info about a session.
608 """Get info about a session.
603
609
604 Parameters
610 Parameters
605 ----------
611 ----------
606 session : int
612 session : int
607 Session number to retrieve. The current session is 0, and negative
613 Session number to retrieve. The current session is 0, and negative
608 numbers count back from current session, so -1 is the previous session.
614 numbers count back from current session, so -1 is the previous session.
609
615
610 Returns
616 Returns
611 -------
617 -------
612 session_id : int
618 session_id : int
613 Session ID number
619 Session ID number
614 start : datetime
620 start : datetime
615 Timestamp for the start of the session.
621 Timestamp for the start of the session.
616 end : datetime
622 end : datetime
617 Timestamp for the end of the session, or None if IPython crashed.
623 Timestamp for the end of the session, or None if IPython crashed.
618 num_cmds : int
624 num_cmds : int
619 Number of commands run, or None if IPython crashed.
625 Number of commands run, or None if IPython crashed.
620 remark : unicode
626 remark : unicode
621 A manually set description.
627 A manually set description.
622 """
628 """
623 if session <= 0:
629 if session <= 0:
624 session += self.session_number
630 session += self.session_number
625
631
626 return super(HistoryManager, self).get_session_info(session=session)
632 return super(HistoryManager, self).get_session_info(session=session)
627
633
628 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
634 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
629 """Get input and output history from the current session. Called by
635 """Get input and output history from the current session. Called by
630 get_range, and takes similar parameters."""
636 get_range, and takes similar parameters."""
631 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
637 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
632
638
633 n = len(input_hist)
639 n = len(input_hist)
634 if start < 0:
640 if start < 0:
635 start += n
641 start += n
636 if not stop or (stop > n):
642 if not stop or (stop > n):
637 stop = n
643 stop = n
638 elif stop < 0:
644 elif stop < 0:
639 stop += n
645 stop += n
640
646
641 for i in range(start, stop):
647 for i in range(start, stop):
642 if output:
648 if output:
643 line = (input_hist[i], self.output_hist_reprs.get(i))
649 line = (input_hist[i], self.output_hist_reprs.get(i))
644 else:
650 else:
645 line = input_hist[i]
651 line = input_hist[i]
646 yield (0, i, line)
652 yield (0, i, line)
647
653
648 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
654 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
649 """Retrieve input by session.
655 """Retrieve input by session.
650
656
651 Parameters
657 Parameters
652 ----------
658 ----------
653 session : int
659 session : int
654 Session number to retrieve. The current session is 0, and negative
660 Session number to retrieve. The current session is 0, and negative
655 numbers count back from current session, so -1 is previous session.
661 numbers count back from current session, so -1 is previous session.
656 start : int
662 start : int
657 First line to retrieve.
663 First line to retrieve.
658 stop : int
664 stop : int
659 End of line range (excluded from output itself). If None, retrieve
665 End of line range (excluded from output itself). If None, retrieve
660 to the end of the session.
666 to the end of the session.
661 raw : bool
667 raw : bool
662 If True, return untranslated input
668 If True, return untranslated input
663 output : bool
669 output : bool
664 If True, attempt to include output. This will be 'real' Python
670 If True, attempt to include output. This will be 'real' Python
665 objects for the current session, or text reprs from previous
671 objects for the current session, or text reprs from previous
666 sessions if db_log_output was enabled at the time. Where no output
672 sessions if db_log_output was enabled at the time. Where no output
667 is found, None is used.
673 is found, None is used.
668
674
669 Returns
675 Returns
670 -------
676 -------
671 entries
677 entries
672 An iterator over the desired lines. Each line is a 3-tuple, either
678 An iterator over the desired lines. Each line is a 3-tuple, either
673 (session, line, input) if output is False, or
679 (session, line, input) if output is False, or
674 (session, line, (input, output)) if output is True.
680 (session, line, (input, output)) if output is True.
675 """
681 """
676 if session <= 0:
682 if session <= 0:
677 session += self.session_number
683 session += self.session_number
678 if session==self.session_number: # Current session
684 if session==self.session_number: # Current session
679 return self._get_range_session(start, stop, raw, output)
685 return self._get_range_session(start, stop, raw, output)
680 return super(HistoryManager, self).get_range(session, start, stop, raw,
686 return super(HistoryManager, self).get_range(session, start, stop, raw,
681 output)
687 output)
682
688
683 ## ----------------------------
689 ## ----------------------------
684 ## Methods for storing history:
690 ## Methods for storing history:
685 ## ----------------------------
691 ## ----------------------------
686 def store_inputs(self, line_num, source, source_raw=None):
692 def store_inputs(self, line_num, source, source_raw=None):
687 """Store source and raw input in history and create input cache
693 """Store source and raw input in history and create input cache
688 variables ``_i*``.
694 variables ``_i*``.
689
695
690 Parameters
696 Parameters
691 ----------
697 ----------
692 line_num : int
698 line_num : int
693 The prompt number of this input.
699 The prompt number of this input.
694 source : str
700 source : str
695 Python input.
701 Python input.
696 source_raw : str, optional
702 source_raw : str, optional
697 If given, this is the raw input without any IPython transformations
703 If given, this is the raw input without any IPython transformations
698 applied to it. If not given, ``source`` is used.
704 applied to it. If not given, ``source`` is used.
699 """
705 """
700 if source_raw is None:
706 if source_raw is None:
701 source_raw = source
707 source_raw = source
702 source = source.rstrip('\n')
708 source = source.rstrip('\n')
703 source_raw = source_raw.rstrip('\n')
709 source_raw = source_raw.rstrip('\n')
704
710
705 # do not store exit/quit commands
711 # do not store exit/quit commands
706 if self._exit_re.match(source_raw.strip()):
712 if self._exit_re.match(source_raw.strip()):
707 return
713 return
708
714
709 self.input_hist_parsed.append(source)
715 self.input_hist_parsed.append(source)
710 self.input_hist_raw.append(source_raw)
716 self.input_hist_raw.append(source_raw)
711
717
712 with self.db_input_cache_lock:
718 with self.db_input_cache_lock:
713 self.db_input_cache.append((line_num, source, source_raw))
719 self.db_input_cache.append((line_num, source, source_raw))
714 # Trigger to flush cache and write to DB.
720 # Trigger to flush cache and write to DB.
715 if len(self.db_input_cache) >= self.db_cache_size:
721 if len(self.db_input_cache) >= self.db_cache_size:
716 self.save_flag.set()
722 self.save_flag.set()
717
723
718 # update the auto _i variables
724 # update the auto _i variables
719 self._iii = self._ii
725 self._iii = self._ii
720 self._ii = self._i
726 self._ii = self._i
721 self._i = self._i00
727 self._i = self._i00
722 self._i00 = source_raw
728 self._i00 = source_raw
723
729
724 # hackish access to user namespace to create _i1,_i2... dynamically
730 # hackish access to user namespace to create _i1,_i2... dynamically
725 new_i = '_i%s' % line_num
731 new_i = '_i%s' % line_num
726 to_main = {'_i': self._i,
732 to_main = {'_i': self._i,
727 '_ii': self._ii,
733 '_ii': self._ii,
728 '_iii': self._iii,
734 '_iii': self._iii,
729 new_i : self._i00 }
735 new_i : self._i00 }
730
736
731 if self.shell is not None:
737 if self.shell is not None:
732 self.shell.push(to_main, interactive=False)
738 self.shell.push(to_main, interactive=False)
733
739
734 def store_output(self, line_num):
740 def store_output(self, line_num):
735 """If database output logging is enabled, this saves all the
741 """If database output logging is enabled, this saves all the
736 outputs from the indicated prompt number to the database. It's
742 outputs from the indicated prompt number to the database. It's
737 called by run_cell after code has been executed.
743 called by run_cell after code has been executed.
738
744
739 Parameters
745 Parameters
740 ----------
746 ----------
741 line_num : int
747 line_num : int
742 The line number from which to save outputs
748 The line number from which to save outputs
743 """
749 """
744 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
750 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
745 return
751 return
746 output = self.output_hist_reprs[line_num]
752 output = self.output_hist_reprs[line_num]
747
753
748 with self.db_output_cache_lock:
754 with self.db_output_cache_lock:
749 self.db_output_cache.append((line_num, output))
755 self.db_output_cache.append((line_num, output))
750 if self.db_cache_size <= 1:
756 if self.db_cache_size <= 1:
751 self.save_flag.set()
757 self.save_flag.set()
752
758
753 def _writeout_input_cache(self, conn):
759 def _writeout_input_cache(self, conn):
754 with conn:
760 with conn:
755 for line in self.db_input_cache:
761 for line in self.db_input_cache:
756 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
762 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
757 (self.session_number,)+line)
763 (self.session_number,)+line)
758
764
759 def _writeout_output_cache(self, conn):
765 def _writeout_output_cache(self, conn):
760 with conn:
766 with conn:
761 for line in self.db_output_cache:
767 for line in self.db_output_cache:
762 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
768 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
763 (self.session_number,)+line)
769 (self.session_number,)+line)
764
770
765 @only_when_enabled
771 @only_when_enabled
766 def writeout_cache(self, conn=None):
772 def writeout_cache(self, conn=None):
767 """Write any entries in the cache to the database."""
773 """Write any entries in the cache to the database."""
768 if conn is None:
774 if conn is None:
769 conn = self.db
775 conn = self.db
770
776
771 with self.db_input_cache_lock:
777 with self.db_input_cache_lock:
772 try:
778 try:
773 self._writeout_input_cache(conn)
779 self._writeout_input_cache(conn)
774 except sqlite3.IntegrityError:
780 except sqlite3.IntegrityError:
775 self.new_session(conn)
781 self.new_session(conn)
776 print("ERROR! Session/line number was not unique in",
782 print("ERROR! Session/line number was not unique in",
777 "database. History logging moved to new session",
783 "database. History logging moved to new session",
778 self.session_number)
784 self.session_number)
779 try:
785 try:
780 # Try writing to the new session. If this fails, don't
786 # Try writing to the new session. If this fails, don't
781 # recurse
787 # recurse
782 self._writeout_input_cache(conn)
788 self._writeout_input_cache(conn)
783 except sqlite3.IntegrityError:
789 except sqlite3.IntegrityError:
784 pass
790 pass
785 finally:
791 finally:
786 self.db_input_cache = []
792 self.db_input_cache = []
787
793
788 with self.db_output_cache_lock:
794 with self.db_output_cache_lock:
789 try:
795 try:
790 self._writeout_output_cache(conn)
796 self._writeout_output_cache(conn)
791 except sqlite3.IntegrityError:
797 except sqlite3.IntegrityError:
792 print("!! Session/line number for output was not unique",
798 print("!! Session/line number for output was not unique",
793 "in database. Output will not be stored.")
799 "in database. Output will not be stored.")
794 finally:
800 finally:
795 self.db_output_cache = []
801 self.db_output_cache = []
796
802
797
803
798 class HistorySavingThread(threading.Thread):
804 class HistorySavingThread(threading.Thread):
799 """This thread takes care of writing history to the database, so that
805 """This thread takes care of writing history to the database, so that
800 the UI isn't held up while that happens.
806 the UI isn't held up while that happens.
801
807
802 It waits for the HistoryManager's save_flag to be set, then writes out
808 It waits for the HistoryManager's save_flag to be set, then writes out
803 the history cache. The main thread is responsible for setting the flag when
809 the history cache. The main thread is responsible for setting the flag when
804 the cache size reaches a defined threshold."""
810 the cache size reaches a defined threshold."""
805 daemon = True
811 daemon = True
806 stop_now = False
812 stop_now = False
807 enabled = True
813 enabled = True
808 def __init__(self, history_manager):
814 def __init__(self, history_manager):
809 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
815 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
810 self.history_manager = history_manager
816 self.history_manager = history_manager
811 self.enabled = history_manager.enabled
817 self.enabled = history_manager.enabled
812 atexit.register(self.stop)
818 atexit.register(self.stop)
813
819
814 @only_when_enabled
820 @only_when_enabled
815 def run(self):
821 def run(self):
816 # We need a separate db connection per thread:
822 # We need a separate db connection per thread:
817 try:
823 try:
818 self.db = sqlite3.connect(
824 self.db = sqlite3.connect(
819 str(self.history_manager.hist_file),
825 str(self.history_manager.hist_file),
820 **self.history_manager.connection_options,
826 **self.history_manager.connection_options,
821 )
827 )
822 while True:
828 while True:
823 self.history_manager.save_flag.wait()
829 self.history_manager.save_flag.wait()
824 if self.stop_now:
830 if self.stop_now:
825 self.db.close()
831 self.db.close()
826 return
832 return
827 self.history_manager.save_flag.clear()
833 self.history_manager.save_flag.clear()
828 self.history_manager.writeout_cache(self.db)
834 self.history_manager.writeout_cache(self.db)
829 except Exception as e:
835 except Exception as e:
830 print(("The history saving thread hit an unexpected error (%s)."
836 print(("The history saving thread hit an unexpected error (%s)."
831 "History will not be written to the database.") % repr(e))
837 "History will not be written to the database.") % repr(e))
832
838
833 def stop(self):
839 def stop(self):
834 """This can be called from the main thread to safely stop this thread.
840 """This can be called from the main thread to safely stop this thread.
835
841
836 Note that it does not attempt to write out remaining history before
842 Note that it does not attempt to write out remaining history before
837 exiting. That should be done by calling the HistoryManager's
843 exiting. That should be done by calling the HistoryManager's
838 end_session method."""
844 end_session method."""
839 self.stop_now = True
845 self.stop_now = True
840 self.history_manager.save_flag.set()
846 self.history_manager.save_flag.set()
841 self.join()
847 self.join()
842
848
843
849
844 # To match, e.g. ~5/8-~2/3
850 # To match, e.g. ~5/8-~2/3
845 range_re = re.compile(r"""
851 range_re = re.compile(r"""
846 ((?P<startsess>~?\d+)/)?
852 ((?P<startsess>~?\d+)/)?
847 (?P<start>\d+)?
853 (?P<start>\d+)?
848 ((?P<sep>[\-:])
854 ((?P<sep>[\-:])
849 ((?P<endsess>~?\d+)/)?
855 ((?P<endsess>~?\d+)/)?
850 (?P<end>\d+))?
856 (?P<end>\d+))?
851 $""", re.VERBOSE)
857 $""", re.VERBOSE)
852
858
853
859
854 def extract_hist_ranges(ranges_str):
860 def extract_hist_ranges(ranges_str):
855 """Turn a string of history ranges into 3-tuples of (session, start, stop).
861 """Turn a string of history ranges into 3-tuples of (session, start, stop).
856
862
857 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
863 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
858 session".
864 session".
859
865
860 Examples
866 Examples
861 --------
867 --------
862 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
868 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
863 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
869 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
864 """
870 """
865 if ranges_str == "":
871 if ranges_str == "":
866 yield (0, 1, None) # Everything from current session
872 yield (0, 1, None) # Everything from current session
867 return
873 return
868
874
869 for range_str in ranges_str.split():
875 for range_str in ranges_str.split():
870 rmatch = range_re.match(range_str)
876 rmatch = range_re.match(range_str)
871 if not rmatch:
877 if not rmatch:
872 continue
878 continue
873 start = rmatch.group("start")
879 start = rmatch.group("start")
874 if start:
880 if start:
875 start = int(start)
881 start = int(start)
876 end = rmatch.group("end")
882 end = rmatch.group("end")
877 # If no end specified, get (a, a + 1)
883 # If no end specified, get (a, a + 1)
878 end = int(end) if end else start + 1
884 end = int(end) if end else start + 1
879 else: # start not specified
885 else: # start not specified
880 if not rmatch.group('startsess'): # no startsess
886 if not rmatch.group('startsess'): # no startsess
881 continue
887 continue
882 start = 1
888 start = 1
883 end = None # provide the entire session hist
889 end = None # provide the entire session hist
884
890
885 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
891 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
886 end += 1
892 end += 1
887 startsess = rmatch.group("startsess") or "0"
893 startsess = rmatch.group("startsess") or "0"
888 endsess = rmatch.group("endsess") or startsess
894 endsess = rmatch.group("endsess") or startsess
889 startsess = int(startsess.replace("~","-"))
895 startsess = int(startsess.replace("~","-"))
890 endsess = int(endsess.replace("~","-"))
896 endsess = int(endsess.replace("~","-"))
891 assert endsess >= startsess, "start session must be earlier than end session"
897 assert endsess >= startsess, "start session must be earlier than end session"
892
898
893 if endsess == startsess:
899 if endsess == startsess:
894 yield (startsess, start, end)
900 yield (startsess, start, end)
895 continue
901 continue
896 # Multiple sessions in one range:
902 # Multiple sessions in one range:
897 yield (startsess, start, None)
903 yield (startsess, start, None)
898 for sess in range(startsess+1, endsess):
904 for sess in range(startsess+1, endsess):
899 yield (sess, 1, None)
905 yield (sess, 1, None)
900 yield (endsess, 1, end)
906 yield (endsess, 1, end)
901
907
902
908
903 def _format_lineno(session, line):
909 def _format_lineno(session, line):
904 """Helper function to format line numbers properly."""
910 """Helper function to format line numbers properly."""
905 if session == 0:
911 if session == 0:
906 return str(line)
912 return str(line)
907 return "%s#%s" % (session, line)
913 return "%s#%s" % (session, line)
General Comments 0
You need to be logged in to leave comments. Login now