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