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