##// END OF EJS Templates
Minor fixes as per @Carreau's review
Fernando Perez -
Show More
@@ -1,721 +1,721 b''
1 """ History related magics and functionality """
1 """ History related magics and functionality """
2 #-----------------------------------------------------------------------------
2 #-----------------------------------------------------------------------------
3 # Copyright (C) 2010-2011 The IPython Development Team.
3 # Copyright (C) 2010-2011 The IPython Development Team.
4 #
4 #
5 # Distributed under the terms of the BSD License.
5 # Distributed under the terms of the BSD License.
6 #
6 #
7 # The full license is in the file COPYING.txt, distributed with this software.
7 # The full license is in the file COPYING.txt, distributed with this software.
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 from __future__ import print_function
13 from __future__ import print_function
14
14
15 # Stdlib imports
15 # Stdlib imports
16 import atexit
16 import atexit
17 import datetime
17 import datetime
18 import os
18 import os
19 import re
19 import re
20 try:
20 try:
21 import sqlite3
21 import sqlite3
22 except ImportError:
22 except ImportError:
23 sqlite3 = None
23 sqlite3 = None
24 import threading
24 import threading
25
25
26 # Our own packages
26 # Our own packages
27 from IPython.config.configurable import Configurable
27 from IPython.config.configurable import Configurable
28 from IPython.external.decorator import decorator
28 from IPython.external.decorator import decorator
29 from IPython.utils.path import locate_profile
29 from IPython.utils.path import locate_profile
30 from IPython.utils.traitlets import Bool, Dict, Instance, Integer, List, Unicode
30 from IPython.utils.traitlets import Bool, Dict, Instance, Integer, List, Unicode
31 from IPython.utils.warn import warn
31 from IPython.utils.warn import warn
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Classes and functions
34 # Classes and functions
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
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 needs_sqlite(f,*a,**kw):
55 def needs_sqlite(f,*a,**kw):
56 """return an empty list in the absence of sqlite"""
56 """return an empty list in the absence of sqlite"""
57 if sqlite3 is None:
57 if sqlite3 is None:
58 return []
58 return []
59 else:
59 else:
60 return f(*a,**kw)
60 return f(*a,**kw)
61
61
62
62
63 class HistoryAccessor(Configurable):
63 class HistoryAccessor(Configurable):
64 """Access the history database without adding to it.
64 """Access the history database without adding to it.
65
65
66 This is intended for use by standalone history tools. IPython shells use
66 This is intended for use by standalone history tools. IPython shells use
67 HistoryManager, below, which is a subclass of this."""
67 HistoryManager, below, which is a subclass of this."""
68
68
69 # String holding the path to the history file
69 # String holding the path to the history file
70 hist_file = Unicode(config=True,
70 hist_file = Unicode(config=True,
71 help="""Path to file to use for SQLite history database.
71 help="""Path to file to use for SQLite history database.
72
72
73 By default, IPython will put the history database in the IPython
73 By default, IPython will put the history database in the IPython
74 profile directory. If you would rather share one history among
74 profile directory. If you would rather share one history among
75 profiles, you ca set this value in each, so that they are consistent.
75 profiles, you can set this value in each, so that they are consistent.
76
76
77 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
77 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
78 mounts. If you see IPython hanging, try setting this to something on a
78 mounts. If you see IPython hanging, try setting this to something on a
79 local disk, e.g::
79 local disk, e.g::
80
80
81 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
81 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
82
82
83 """)
83 """)
84
84
85 # The SQLite database
85 # The SQLite database
86 if sqlite3:
86 if sqlite3:
87 db = Instance(sqlite3.Connection)
87 db = Instance(sqlite3.Connection)
88 else:
88 else:
89 db = Instance(DummyDB)
89 db = Instance(DummyDB)
90
90
91 def __init__(self, profile='default', hist_file=u'', config=None, **traits):
91 def __init__(self, profile='default', hist_file=u'', config=None, **traits):
92 """Create a new history accessor.
92 """Create a new history accessor.
93
93
94 Parameters
94 Parameters
95 ----------
95 ----------
96 profile : str
96 profile : str
97 The name of the profile from which to open history.
97 The name of the profile from which to open history.
98 hist_file : str
98 hist_file : str
99 Path to an SQLite history database stored by IPython. If specified,
99 Path to an SQLite history database stored by IPython. If specified,
100 hist_file overrides profile.
100 hist_file overrides profile.
101 config :
101 config :
102 Config object. hist_file can also be set through this.
102 Config object. hist_file can also be set through this.
103 """
103 """
104 # We need a pointer back to the shell for various tasks.
104 # We need a pointer back to the shell for various tasks.
105 super(HistoryAccessor, self).__init__(config=config, **traits)
105 super(HistoryAccessor, self).__init__(config=config, **traits)
106 # defer setting hist_file from kwarg until after init,
106 # defer setting hist_file from kwarg until after init,
107 # otherwise the default kwarg value would clobber any value
107 # otherwise the default kwarg value would clobber any value
108 # set by config
108 # set by config
109 if hist_file:
109 if hist_file:
110 self.hist_file = hist_file
110 self.hist_file = hist_file
111
111
112 if self.hist_file == u'':
112 if self.hist_file == u'':
113 # No one has set the hist_file, yet.
113 # No one has set the hist_file, yet.
114 self.hist_file = self._get_hist_file_name(profile)
114 self.hist_file = self._get_hist_file_name(profile)
115
115
116 if sqlite3 is None:
116 if sqlite3 is None:
117 warn("IPython History requires SQLite, your history will not be saved\n")
117 warn("IPython History requires SQLite, your history will not be saved\n")
118 self.db = DummyDB()
118 self.db = DummyDB()
119 return
119 return
120
120
121 try:
121 try:
122 self.init_db()
122 self.init_db()
123 except sqlite3.DatabaseError:
123 except sqlite3.DatabaseError:
124 if os.path.isfile(self.hist_file):
124 if os.path.isfile(self.hist_file):
125 # Try to move the file out of the way
125 # Try to move the file out of the way
126 base,ext = os.path.splitext(self.hist_file)
126 base,ext = os.path.splitext(self.hist_file)
127 newpath = base + '-corrupt' + ext
127 newpath = base + '-corrupt' + ext
128 os.rename(self.hist_file, newpath)
128 os.rename(self.hist_file, newpath)
129 print("ERROR! History file wasn't a valid SQLite database.",
129 print("ERROR! History file wasn't a valid SQLite database.",
130 "It was moved to %s" % newpath, "and a new file created.")
130 "It was moved to %s" % newpath, "and a new file created.")
131 self.init_db()
131 self.init_db()
132 else:
132 else:
133 # The hist_file is probably :memory: or something else.
133 # The hist_file is probably :memory: or something else.
134 raise
134 raise
135
135
136 def _get_hist_file_name(self, profile='default'):
136 def _get_hist_file_name(self, profile='default'):
137 """Find the history file for the given profile name.
137 """Find the history file for the given profile name.
138
138
139 This is overridden by the HistoryManager subclass, to use the shell's
139 This is overridden by the HistoryManager subclass, to use the shell's
140 active profile.
140 active profile.
141
141
142 Parameters
142 Parameters
143 ----------
143 ----------
144 profile : str
144 profile : str
145 The name of a profile which has a history file.
145 The name of a profile which has a history file.
146 """
146 """
147 return os.path.join(locate_profile(profile), 'history.sqlite')
147 return os.path.join(locate_profile(profile), 'history.sqlite')
148
148
149 def init_db(self):
149 def init_db(self):
150 """Connect to the database, and create tables if necessary."""
150 """Connect to the database, and create tables if necessary."""
151 # use detect_types so that timestamps return datetime objects
151 # use detect_types so that timestamps return datetime objects
152 self.db = sqlite3.connect(self.hist_file,
152 self.db = sqlite3.connect(self.hist_file,
153 detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
153 detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
154 self.db.execute("""CREATE TABLE IF NOT EXISTS sessions (session integer
154 self.db.execute("""CREATE TABLE IF NOT EXISTS sessions (session integer
155 primary key autoincrement, start timestamp,
155 primary key autoincrement, start timestamp,
156 end timestamp, num_cmds integer, remark text)""")
156 end timestamp, num_cmds integer, remark text)""")
157 self.db.execute("""CREATE TABLE IF NOT EXISTS history
157 self.db.execute("""CREATE TABLE IF NOT EXISTS history
158 (session integer, line integer, source text, source_raw text,
158 (session integer, line integer, source text, source_raw text,
159 PRIMARY KEY (session, line))""")
159 PRIMARY KEY (session, line))""")
160 # Output history is optional, but ensure the table's there so it can be
160 # Output history is optional, but ensure the table's there so it can be
161 # enabled later.
161 # enabled later.
162 self.db.execute("""CREATE TABLE IF NOT EXISTS output_history
162 self.db.execute("""CREATE TABLE IF NOT EXISTS output_history
163 (session integer, line integer, output text,
163 (session integer, line integer, output text,
164 PRIMARY KEY (session, line))""")
164 PRIMARY KEY (session, line))""")
165 self.db.commit()
165 self.db.commit()
166
166
167 def writeout_cache(self):
167 def writeout_cache(self):
168 """Overridden by HistoryManager to dump the cache before certain
168 """Overridden by HistoryManager to dump the cache before certain
169 database lookups."""
169 database lookups."""
170 pass
170 pass
171
171
172 ## -------------------------------
172 ## -------------------------------
173 ## Methods for retrieving history:
173 ## Methods for retrieving history:
174 ## -------------------------------
174 ## -------------------------------
175 def _run_sql(self, sql, params, raw=True, output=False):
175 def _run_sql(self, sql, params, raw=True, output=False):
176 """Prepares and runs an SQL query for the history database.
176 """Prepares and runs an SQL query for the history database.
177
177
178 Parameters
178 Parameters
179 ----------
179 ----------
180 sql : str
180 sql : str
181 Any filtering expressions to go after SELECT ... FROM ...
181 Any filtering expressions to go after SELECT ... FROM ...
182 params : tuple
182 params : tuple
183 Parameters passed to the SQL query (to replace "?")
183 Parameters passed to the SQL query (to replace "?")
184 raw, output : bool
184 raw, output : bool
185 See :meth:`get_range`
185 See :meth:`get_range`
186
186
187 Returns
187 Returns
188 -------
188 -------
189 Tuples as :meth:`get_range`
189 Tuples as :meth:`get_range`
190 """
190 """
191 toget = 'source_raw' if raw else 'source'
191 toget = 'source_raw' if raw else 'source'
192 sqlfrom = "history"
192 sqlfrom = "history"
193 if output:
193 if output:
194 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
194 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
195 toget = "history.%s, output_history.output" % toget
195 toget = "history.%s, output_history.output" % toget
196 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
196 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
197 (toget, sqlfrom) + sql, params)
197 (toget, sqlfrom) + sql, params)
198 if output: # Regroup into 3-tuples, and parse JSON
198 if output: # Regroup into 3-tuples, and parse JSON
199 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
199 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
200 return cur
200 return cur
201
201
202 @needs_sqlite
202 @needs_sqlite
203 def get_session_info(self, session=0):
203 def get_session_info(self, session=0):
204 """get info about a session
204 """get info about a session
205
205
206 Parameters
206 Parameters
207 ----------
207 ----------
208
208
209 session : int
209 session : int
210 Session number to retrieve. The current session is 0, and negative
210 Session number to retrieve. The current session is 0, and negative
211 numbers count back from current session, so -1 is previous session.
211 numbers count back from current session, so -1 is previous session.
212
212
213 Returns
213 Returns
214 -------
214 -------
215
215
216 (session_id [int], start [datetime], end [datetime], num_cmds [int],
216 (session_id [int], start [datetime], end [datetime], num_cmds [int],
217 remark [unicode])
217 remark [unicode])
218
218
219 Sessions that are running or did not exit cleanly will have `end=None`
219 Sessions that are running or did not exit cleanly will have `end=None`
220 and `num_cmds=None`.
220 and `num_cmds=None`.
221
221
222 """
222 """
223
223
224 if session <= 0:
224 if session <= 0:
225 session += self.session_number
225 session += self.session_number
226
226
227 query = "SELECT * from sessions where session == ?"
227 query = "SELECT * from sessions where session == ?"
228 return self.db.execute(query, (session,)).fetchone()
228 return self.db.execute(query, (session,)).fetchone()
229
229
230 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
230 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
231 """Get the last n lines from the history database.
231 """Get the last n lines from the history database.
232
232
233 Parameters
233 Parameters
234 ----------
234 ----------
235 n : int
235 n : int
236 The number of lines to get
236 The number of lines to get
237 raw, output : bool
237 raw, output : bool
238 See :meth:`get_range`
238 See :meth:`get_range`
239 include_latest : bool
239 include_latest : bool
240 If False (default), n+1 lines are fetched, and the latest one
240 If False (default), n+1 lines are fetched, and the latest one
241 is discarded. This is intended to be used where the function
241 is discarded. This is intended to be used where the function
242 is called by a user command, which it should not return.
242 is called by a user command, which it should not return.
243
243
244 Returns
244 Returns
245 -------
245 -------
246 Tuples as :meth:`get_range`
246 Tuples as :meth:`get_range`
247 """
247 """
248 self.writeout_cache()
248 self.writeout_cache()
249 if not include_latest:
249 if not include_latest:
250 n += 1
250 n += 1
251 cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
251 cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
252 (n,), raw=raw, output=output)
252 (n,), raw=raw, output=output)
253 if not include_latest:
253 if not include_latest:
254 return reversed(list(cur)[1:])
254 return reversed(list(cur)[1:])
255 return reversed(list(cur))
255 return reversed(list(cur))
256
256
257 def search(self, pattern="*", raw=True, search_raw=True,
257 def search(self, pattern="*", raw=True, search_raw=True,
258 output=False):
258 output=False):
259 """Search the database using unix glob-style matching (wildcards
259 """Search the database using unix glob-style matching (wildcards
260 * and ?).
260 * and ?).
261
261
262 Parameters
262 Parameters
263 ----------
263 ----------
264 pattern : str
264 pattern : str
265 The wildcarded pattern to match when searching
265 The wildcarded pattern to match when searching
266 search_raw : bool
266 search_raw : bool
267 If True, search the raw input, otherwise, the parsed input
267 If True, search the raw input, otherwise, the parsed input
268 raw, output : bool
268 raw, output : bool
269 See :meth:`get_range`
269 See :meth:`get_range`
270
270
271 Returns
271 Returns
272 -------
272 -------
273 Tuples as :meth:`get_range`
273 Tuples as :meth:`get_range`
274 """
274 """
275 tosearch = "source_raw" if search_raw else "source"
275 tosearch = "source_raw" if search_raw else "source"
276 if output:
276 if output:
277 tosearch = "history." + tosearch
277 tosearch = "history." + tosearch
278 self.writeout_cache()
278 self.writeout_cache()
279 return self._run_sql("WHERE %s GLOB ?" % tosearch, (pattern,),
279 return self._run_sql("WHERE %s GLOB ?" % tosearch, (pattern,),
280 raw=raw, output=output)
280 raw=raw, output=output)
281
281
282 def get_range(self, session, start=1, stop=None, raw=True,output=False):
282 def get_range(self, session, start=1, stop=None, raw=True,output=False):
283 """Retrieve input by session.
283 """Retrieve input by session.
284
284
285 Parameters
285 Parameters
286 ----------
286 ----------
287 session : int
287 session : int
288 Session number to retrieve.
288 Session number to retrieve.
289 start : int
289 start : int
290 First line to retrieve.
290 First line to retrieve.
291 stop : int
291 stop : int
292 End of line range (excluded from output itself). If None, retrieve
292 End of line range (excluded from output itself). If None, retrieve
293 to the end of the session.
293 to the end of the session.
294 raw : bool
294 raw : bool
295 If True, return untranslated input
295 If True, return untranslated input
296 output : bool
296 output : bool
297 If True, attempt to include output. This will be 'real' Python
297 If True, attempt to include output. This will be 'real' Python
298 objects for the current session, or text reprs from previous
298 objects for the current session, or text reprs from previous
299 sessions if db_log_output was enabled at the time. Where no output
299 sessions if db_log_output was enabled at the time. Where no output
300 is found, None is used.
300 is found, None is used.
301
301
302 Returns
302 Returns
303 -------
303 -------
304 An iterator over the desired lines. Each line is a 3-tuple, either
304 An iterator over the desired lines. Each line is a 3-tuple, either
305 (session, line, input) if output is False, or
305 (session, line, input) if output is False, or
306 (session, line, (input, output)) if output is True.
306 (session, line, (input, output)) if output is True.
307 """
307 """
308 if stop:
308 if stop:
309 lineclause = "line >= ? AND line < ?"
309 lineclause = "line >= ? AND line < ?"
310 params = (session, start, stop)
310 params = (session, start, stop)
311 else:
311 else:
312 lineclause = "line>=?"
312 lineclause = "line>=?"
313 params = (session, start)
313 params = (session, start)
314
314
315 return self._run_sql("WHERE session==? AND %s""" % lineclause,
315 return self._run_sql("WHERE session==? AND %s""" % lineclause,
316 params, raw=raw, output=output)
316 params, raw=raw, output=output)
317
317
318 def get_range_by_str(self, rangestr, raw=True, output=False):
318 def get_range_by_str(self, rangestr, raw=True, output=False):
319 """Get lines of history from a string of ranges, as used by magic
319 """Get lines of history from a string of ranges, as used by magic
320 commands %hist, %save, %macro, etc.
320 commands %hist, %save, %macro, etc.
321
321
322 Parameters
322 Parameters
323 ----------
323 ----------
324 rangestr : str
324 rangestr : str
325 A string specifying ranges, e.g. "5 ~2/1-4". See
325 A string specifying ranges, e.g. "5 ~2/1-4". See
326 :func:`magic_history` for full details.
326 :func:`magic_history` for full details.
327 raw, output : bool
327 raw, output : bool
328 As :meth:`get_range`
328 As :meth:`get_range`
329
329
330 Returns
330 Returns
331 -------
331 -------
332 Tuples as :meth:`get_range`
332 Tuples as :meth:`get_range`
333 """
333 """
334 for sess, s, e in extract_hist_ranges(rangestr):
334 for sess, s, e in extract_hist_ranges(rangestr):
335 for line in self.get_range(sess, s, e, raw=raw, output=output):
335 for line in self.get_range(sess, s, e, raw=raw, output=output):
336 yield line
336 yield line
337
337
338
338
339 class HistoryManager(HistoryAccessor):
339 class HistoryManager(HistoryAccessor):
340 """A class to organize all history-related functionality in one place.
340 """A class to organize all history-related functionality in one place.
341 """
341 """
342 # Public interface
342 # Public interface
343
343
344 # An instance of the IPython shell we are attached to
344 # An instance of the IPython shell we are attached to
345 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
345 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
346 # Lists to hold processed and raw history. These start with a blank entry
346 # Lists to hold processed and raw history. These start with a blank entry
347 # so that we can index them starting from 1
347 # so that we can index them starting from 1
348 input_hist_parsed = List([""])
348 input_hist_parsed = List([""])
349 input_hist_raw = List([""])
349 input_hist_raw = List([""])
350 # A list of directories visited during session
350 # A list of directories visited during session
351 dir_hist = List()
351 dir_hist = List()
352 def _dir_hist_default(self):
352 def _dir_hist_default(self):
353 try:
353 try:
354 return [os.getcwdu()]
354 return [os.getcwdu()]
355 except OSError:
355 except OSError:
356 return []
356 return []
357
357
358 # A dict of output history, keyed with ints from the shell's
358 # A dict of output history, keyed with ints from the shell's
359 # execution count.
359 # execution count.
360 output_hist = Dict()
360 output_hist = Dict()
361 # The text/plain repr of outputs.
361 # The text/plain repr of outputs.
362 output_hist_reprs = Dict()
362 output_hist_reprs = Dict()
363
363
364 # The number of the current session in the history database
364 # The number of the current session in the history database
365 session_number = Integer()
365 session_number = Integer()
366 # Should we log output to the database? (default no)
366 # Should we log output to the database? (default no)
367 db_log_output = Bool(False, config=True)
367 db_log_output = Bool(False, config=True)
368 # Write to database every x commands (higher values save disk access & power)
368 # Write to database every x commands (higher values save disk access & power)
369 # Values of 1 or less effectively disable caching.
369 # Values of 1 or less effectively disable caching.
370 db_cache_size = Integer(0, config=True)
370 db_cache_size = Integer(0, config=True)
371 # The input and output caches
371 # The input and output caches
372 db_input_cache = List()
372 db_input_cache = List()
373 db_output_cache = List()
373 db_output_cache = List()
374
374
375 # History saving in separate thread
375 # History saving in separate thread
376 save_thread = Instance('IPython.core.history.HistorySavingThread')
376 save_thread = Instance('IPython.core.history.HistorySavingThread')
377 try: # Event is a function returning an instance of _Event...
377 try: # Event is a function returning an instance of _Event...
378 save_flag = Instance(threading._Event)
378 save_flag = Instance(threading._Event)
379 except AttributeError: # ...until Python 3.3, when it's a class.
379 except AttributeError: # ...until Python 3.3, when it's a class.
380 save_flag = Instance(threading.Event)
380 save_flag = Instance(threading.Event)
381
381
382 # Private interface
382 # Private interface
383 # Variables used to store the three last inputs from the user. On each new
383 # Variables used to store the three last inputs from the user. On each new
384 # history update, we populate the user's namespace with these, shifted as
384 # history update, we populate the user's namespace with these, shifted as
385 # necessary.
385 # necessary.
386 _i00 = Unicode(u'')
386 _i00 = Unicode(u'')
387 _i = Unicode(u'')
387 _i = Unicode(u'')
388 _ii = Unicode(u'')
388 _ii = Unicode(u'')
389 _iii = Unicode(u'')
389 _iii = Unicode(u'')
390
390
391 # A regex matching all forms of the exit command, so that we don't store
391 # A regex matching all forms of the exit command, so that we don't store
392 # them in the history (it's annoying to rewind the first entry and land on
392 # them in the history (it's annoying to rewind the first entry and land on
393 # an exit call).
393 # an exit call).
394 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
394 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
395
395
396 def __init__(self, shell=None, config=None, **traits):
396 def __init__(self, shell=None, config=None, **traits):
397 """Create a new history manager associated with a shell instance.
397 """Create a new history manager associated with a shell instance.
398 """
398 """
399 # We need a pointer back to the shell for various tasks.
399 # We need a pointer back to the shell for various tasks.
400 super(HistoryManager, self).__init__(shell=shell, config=config,
400 super(HistoryManager, self).__init__(shell=shell, config=config,
401 **traits)
401 **traits)
402 self.save_flag = threading.Event()
402 self.save_flag = threading.Event()
403 self.db_input_cache_lock = threading.Lock()
403 self.db_input_cache_lock = threading.Lock()
404 self.db_output_cache_lock = threading.Lock()
404 self.db_output_cache_lock = threading.Lock()
405 if self.hist_file != ':memory:':
405 if self.hist_file != ':memory:':
406 self.save_thread = HistorySavingThread(self)
406 self.save_thread = HistorySavingThread(self)
407 self.save_thread.start()
407 self.save_thread.start()
408
408
409 self.new_session()
409 self.new_session()
410
410
411 def _get_hist_file_name(self, profile=None):
411 def _get_hist_file_name(self, profile=None):
412 """Get default history file name based on the Shell's profile.
412 """Get default history file name based on the Shell's profile.
413
413
414 The profile parameter is ignored, but must exist for compatibility with
414 The profile parameter is ignored, but must exist for compatibility with
415 the parent class."""
415 the parent class."""
416 profile_dir = self.shell.profile_dir.location
416 profile_dir = self.shell.profile_dir.location
417 return os.path.join(profile_dir, 'history.sqlite')
417 return os.path.join(profile_dir, 'history.sqlite')
418
418
419 @needs_sqlite
419 @needs_sqlite
420 def new_session(self, conn=None):
420 def new_session(self, conn=None):
421 """Get a new session number."""
421 """Get a new session number."""
422 if conn is None:
422 if conn is None:
423 conn = self.db
423 conn = self.db
424
424
425 with conn:
425 with conn:
426 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
426 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
427 NULL, "") """, (datetime.datetime.now(),))
427 NULL, "") """, (datetime.datetime.now(),))
428 self.session_number = cur.lastrowid
428 self.session_number = cur.lastrowid
429
429
430 def end_session(self):
430 def end_session(self):
431 """Close the database session, filling in the end time and line count."""
431 """Close the database session, filling in the end time and line count."""
432 self.writeout_cache()
432 self.writeout_cache()
433 with self.db:
433 with self.db:
434 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
434 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
435 session==?""", (datetime.datetime.now(),
435 session==?""", (datetime.datetime.now(),
436 len(self.input_hist_parsed)-1, self.session_number))
436 len(self.input_hist_parsed)-1, self.session_number))
437 self.session_number = 0
437 self.session_number = 0
438
438
439 def name_session(self, name):
439 def name_session(self, name):
440 """Give the current session a name in the history database."""
440 """Give the current session a name in the history database."""
441 with self.db:
441 with self.db:
442 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
442 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
443 (name, self.session_number))
443 (name, self.session_number))
444
444
445 def reset(self, new_session=True):
445 def reset(self, new_session=True):
446 """Clear the session history, releasing all object references, and
446 """Clear the session history, releasing all object references, and
447 optionally open a new session."""
447 optionally open a new session."""
448 self.output_hist.clear()
448 self.output_hist.clear()
449 # The directory history can't be completely empty
449 # The directory history can't be completely empty
450 self.dir_hist[:] = [os.getcwdu()]
450 self.dir_hist[:] = [os.getcwdu()]
451
451
452 if new_session:
452 if new_session:
453 if self.session_number:
453 if self.session_number:
454 self.end_session()
454 self.end_session()
455 self.input_hist_parsed[:] = [""]
455 self.input_hist_parsed[:] = [""]
456 self.input_hist_raw[:] = [""]
456 self.input_hist_raw[:] = [""]
457 self.new_session()
457 self.new_session()
458
458
459 # ------------------------------
459 # ------------------------------
460 # Methods for retrieving history
460 # Methods for retrieving history
461 # ------------------------------
461 # ------------------------------
462 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
462 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
463 """Get input and output history from the current session. Called by
463 """Get input and output history from the current session. Called by
464 get_range, and takes similar parameters."""
464 get_range, and takes similar parameters."""
465 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
465 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
466
466
467 n = len(input_hist)
467 n = len(input_hist)
468 if start < 0:
468 if start < 0:
469 start += n
469 start += n
470 if not stop or (stop > n):
470 if not stop or (stop > n):
471 stop = n
471 stop = n
472 elif stop < 0:
472 elif stop < 0:
473 stop += n
473 stop += n
474
474
475 for i in range(start, stop):
475 for i in range(start, stop):
476 if output:
476 if output:
477 line = (input_hist[i], self.output_hist_reprs.get(i))
477 line = (input_hist[i], self.output_hist_reprs.get(i))
478 else:
478 else:
479 line = input_hist[i]
479 line = input_hist[i]
480 yield (0, i, line)
480 yield (0, i, line)
481
481
482 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
482 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
483 """Retrieve input by session.
483 """Retrieve input by session.
484
484
485 Parameters
485 Parameters
486 ----------
486 ----------
487 session : int
487 session : int
488 Session number to retrieve. The current session is 0, and negative
488 Session number to retrieve. The current session is 0, and negative
489 numbers count back from current session, so -1 is previous session.
489 numbers count back from current session, so -1 is previous session.
490 start : int
490 start : int
491 First line to retrieve.
491 First line to retrieve.
492 stop : int
492 stop : int
493 End of line range (excluded from output itself). If None, retrieve
493 End of line range (excluded from output itself). If None, retrieve
494 to the end of the session.
494 to the end of the session.
495 raw : bool
495 raw : bool
496 If True, return untranslated input
496 If True, return untranslated input
497 output : bool
497 output : bool
498 If True, attempt to include output. This will be 'real' Python
498 If True, attempt to include output. This will be 'real' Python
499 objects for the current session, or text reprs from previous
499 objects for the current session, or text reprs from previous
500 sessions if db_log_output was enabled at the time. Where no output
500 sessions if db_log_output was enabled at the time. Where no output
501 is found, None is used.
501 is found, None is used.
502
502
503 Returns
503 Returns
504 -------
504 -------
505 An iterator over the desired lines. Each line is a 3-tuple, either
505 An iterator over the desired lines. Each line is a 3-tuple, either
506 (session, line, input) if output is False, or
506 (session, line, input) if output is False, or
507 (session, line, (input, output)) if output is True.
507 (session, line, (input, output)) if output is True.
508 """
508 """
509 if session <= 0:
509 if session <= 0:
510 session += self.session_number
510 session += self.session_number
511 if session==self.session_number: # Current session
511 if session==self.session_number: # Current session
512 return self._get_range_session(start, stop, raw, output)
512 return self._get_range_session(start, stop, raw, output)
513 return super(HistoryManager, self).get_range(session, start, stop, raw,
513 return super(HistoryManager, self).get_range(session, start, stop, raw,
514 output)
514 output)
515
515
516 ## ----------------------------
516 ## ----------------------------
517 ## Methods for storing history:
517 ## Methods for storing history:
518 ## ----------------------------
518 ## ----------------------------
519 def store_inputs(self, line_num, source, source_raw=None):
519 def store_inputs(self, line_num, source, source_raw=None):
520 """Store source and raw input in history and create input cache
520 """Store source and raw input in history and create input cache
521 variables _i*.
521 variables _i*.
522
522
523 Parameters
523 Parameters
524 ----------
524 ----------
525 line_num : int
525 line_num : int
526 The prompt number of this input.
526 The prompt number of this input.
527
527
528 source : str
528 source : str
529 Python input.
529 Python input.
530
530
531 source_raw : str, optional
531 source_raw : str, optional
532 If given, this is the raw input without any IPython transformations
532 If given, this is the raw input without any IPython transformations
533 applied to it. If not given, ``source`` is used.
533 applied to it. If not given, ``source`` is used.
534 """
534 """
535 if source_raw is None:
535 if source_raw is None:
536 source_raw = source
536 source_raw = source
537 source = source.rstrip('\n')
537 source = source.rstrip('\n')
538 source_raw = source_raw.rstrip('\n')
538 source_raw = source_raw.rstrip('\n')
539
539
540 # do not store exit/quit commands
540 # do not store exit/quit commands
541 if self._exit_re.match(source_raw.strip()):
541 if self._exit_re.match(source_raw.strip()):
542 return
542 return
543
543
544 self.input_hist_parsed.append(source)
544 self.input_hist_parsed.append(source)
545 self.input_hist_raw.append(source_raw)
545 self.input_hist_raw.append(source_raw)
546
546
547 with self.db_input_cache_lock:
547 with self.db_input_cache_lock:
548 self.db_input_cache.append((line_num, source, source_raw))
548 self.db_input_cache.append((line_num, source, source_raw))
549 # Trigger to flush cache and write to DB.
549 # Trigger to flush cache and write to DB.
550 if len(self.db_input_cache) >= self.db_cache_size:
550 if len(self.db_input_cache) >= self.db_cache_size:
551 self.save_flag.set()
551 self.save_flag.set()
552
552
553 # update the auto _i variables
553 # update the auto _i variables
554 self._iii = self._ii
554 self._iii = self._ii
555 self._ii = self._i
555 self._ii = self._i
556 self._i = self._i00
556 self._i = self._i00
557 self._i00 = source_raw
557 self._i00 = source_raw
558
558
559 # hackish access to user namespace to create _i1,_i2... dynamically
559 # hackish access to user namespace to create _i1,_i2... dynamically
560 new_i = '_i%s' % line_num
560 new_i = '_i%s' % line_num
561 to_main = {'_i': self._i,
561 to_main = {'_i': self._i,
562 '_ii': self._ii,
562 '_ii': self._ii,
563 '_iii': self._iii,
563 '_iii': self._iii,
564 new_i : self._i00 }
564 new_i : self._i00 }
565
565
566 self.shell.push(to_main, interactive=False)
566 self.shell.push(to_main, interactive=False)
567
567
568 def store_output(self, line_num):
568 def store_output(self, line_num):
569 """If database output logging is enabled, this saves all the
569 """If database output logging is enabled, this saves all the
570 outputs from the indicated prompt number to the database. It's
570 outputs from the indicated prompt number to the database. It's
571 called by run_cell after code has been executed.
571 called by run_cell after code has been executed.
572
572
573 Parameters
573 Parameters
574 ----------
574 ----------
575 line_num : int
575 line_num : int
576 The line number from which to save outputs
576 The line number from which to save outputs
577 """
577 """
578 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
578 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
579 return
579 return
580 output = self.output_hist_reprs[line_num]
580 output = self.output_hist_reprs[line_num]
581
581
582 with self.db_output_cache_lock:
582 with self.db_output_cache_lock:
583 self.db_output_cache.append((line_num, output))
583 self.db_output_cache.append((line_num, output))
584 if self.db_cache_size <= 1:
584 if self.db_cache_size <= 1:
585 self.save_flag.set()
585 self.save_flag.set()
586
586
587 def _writeout_input_cache(self, conn):
587 def _writeout_input_cache(self, conn):
588 with conn:
588 with conn:
589 for line in self.db_input_cache:
589 for line in self.db_input_cache:
590 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
590 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
591 (self.session_number,)+line)
591 (self.session_number,)+line)
592
592
593 def _writeout_output_cache(self, conn):
593 def _writeout_output_cache(self, conn):
594 with conn:
594 with conn:
595 for line in self.db_output_cache:
595 for line in self.db_output_cache:
596 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
596 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
597 (self.session_number,)+line)
597 (self.session_number,)+line)
598
598
599 @needs_sqlite
599 @needs_sqlite
600 def writeout_cache(self, conn=None):
600 def writeout_cache(self, conn=None):
601 """Write any entries in the cache to the database."""
601 """Write any entries in the cache to the database."""
602 if conn is None:
602 if conn is None:
603 conn = self.db
603 conn = self.db
604
604
605 with self.db_input_cache_lock:
605 with self.db_input_cache_lock:
606 try:
606 try:
607 self._writeout_input_cache(conn)
607 self._writeout_input_cache(conn)
608 except sqlite3.IntegrityError:
608 except sqlite3.IntegrityError:
609 self.new_session(conn)
609 self.new_session(conn)
610 print("ERROR! Session/line number was not unique in",
610 print("ERROR! Session/line number was not unique in",
611 "database. History logging moved to new session",
611 "database. History logging moved to new session",
612 self.session_number)
612 self.session_number)
613 try:
613 try:
614 # Try writing to the new session. If this fails, don't
614 # Try writing to the new session. If this fails, don't
615 # recurse
615 # recurse
616 self._writeout_input_cache(conn)
616 self._writeout_input_cache(conn)
617 except sqlite3.IntegrityError:
617 except sqlite3.IntegrityError:
618 pass
618 pass
619 finally:
619 finally:
620 self.db_input_cache = []
620 self.db_input_cache = []
621
621
622 with self.db_output_cache_lock:
622 with self.db_output_cache_lock:
623 try:
623 try:
624 self._writeout_output_cache(conn)
624 self._writeout_output_cache(conn)
625 except sqlite3.IntegrityError:
625 except sqlite3.IntegrityError:
626 print("!! Session/line number for output was not unique",
626 print("!! Session/line number for output was not unique",
627 "in database. Output will not be stored.")
627 "in database. Output will not be stored.")
628 finally:
628 finally:
629 self.db_output_cache = []
629 self.db_output_cache = []
630
630
631
631
632 class HistorySavingThread(threading.Thread):
632 class HistorySavingThread(threading.Thread):
633 """This thread takes care of writing history to the database, so that
633 """This thread takes care of writing history to the database, so that
634 the UI isn't held up while that happens.
634 the UI isn't held up while that happens.
635
635
636 It waits for the HistoryManager's save_flag to be set, then writes out
636 It waits for the HistoryManager's save_flag to be set, then writes out
637 the history cache. The main thread is responsible for setting the flag when
637 the history cache. The main thread is responsible for setting the flag when
638 the cache size reaches a defined threshold."""
638 the cache size reaches a defined threshold."""
639 daemon = True
639 daemon = True
640 stop_now = False
640 stop_now = False
641 def __init__(self, history_manager):
641 def __init__(self, history_manager):
642 super(HistorySavingThread, self).__init__()
642 super(HistorySavingThread, self).__init__()
643 self.history_manager = history_manager
643 self.history_manager = history_manager
644 atexit.register(self.stop)
644 atexit.register(self.stop)
645
645
646 @needs_sqlite
646 @needs_sqlite
647 def run(self):
647 def run(self):
648 # We need a separate db connection per thread:
648 # We need a separate db connection per thread:
649 try:
649 try:
650 self.db = sqlite3.connect(self.history_manager.hist_file)
650 self.db = sqlite3.connect(self.history_manager.hist_file)
651 while True:
651 while True:
652 self.history_manager.save_flag.wait()
652 self.history_manager.save_flag.wait()
653 if self.stop_now:
653 if self.stop_now:
654 return
654 return
655 self.history_manager.save_flag.clear()
655 self.history_manager.save_flag.clear()
656 self.history_manager.writeout_cache(self.db)
656 self.history_manager.writeout_cache(self.db)
657 except Exception as e:
657 except Exception as e:
658 print(("The history saving thread hit an unexpected error (%s)."
658 print(("The history saving thread hit an unexpected error (%s)."
659 "History will not be written to the database.") % repr(e))
659 "History will not be written to the database.") % repr(e))
660
660
661 def stop(self):
661 def stop(self):
662 """This can be called from the main thread to safely stop this thread.
662 """This can be called from the main thread to safely stop this thread.
663
663
664 Note that it does not attempt to write out remaining history before
664 Note that it does not attempt to write out remaining history before
665 exiting. That should be done by calling the HistoryManager's
665 exiting. That should be done by calling the HistoryManager's
666 end_session method."""
666 end_session method."""
667 self.stop_now = True
667 self.stop_now = True
668 self.history_manager.save_flag.set()
668 self.history_manager.save_flag.set()
669 self.join()
669 self.join()
670
670
671
671
672 # To match, e.g. ~5/8-~2/3
672 # To match, e.g. ~5/8-~2/3
673 range_re = re.compile(r"""
673 range_re = re.compile(r"""
674 ((?P<startsess>~?\d+)/)?
674 ((?P<startsess>~?\d+)/)?
675 (?P<start>\d+) # Only the start line num is compulsory
675 (?P<start>\d+) # Only the start line num is compulsory
676 ((?P<sep>[\-:])
676 ((?P<sep>[\-:])
677 ((?P<endsess>~?\d+)/)?
677 ((?P<endsess>~?\d+)/)?
678 (?P<end>\d+))?
678 (?P<end>\d+))?
679 $""", re.VERBOSE)
679 $""", re.VERBOSE)
680
680
681
681
682 def extract_hist_ranges(ranges_str):
682 def extract_hist_ranges(ranges_str):
683 """Turn a string of history ranges into 3-tuples of (session, start, stop).
683 """Turn a string of history ranges into 3-tuples of (session, start, stop).
684
684
685 Examples
685 Examples
686 --------
686 --------
687 list(extract_input_ranges("~8/5-~7/4 2"))
687 list(extract_input_ranges("~8/5-~7/4 2"))
688 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
688 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
689 """
689 """
690 for range_str in ranges_str.split():
690 for range_str in ranges_str.split():
691 rmatch = range_re.match(range_str)
691 rmatch = range_re.match(range_str)
692 if not rmatch:
692 if not rmatch:
693 continue
693 continue
694 start = int(rmatch.group("start"))
694 start = int(rmatch.group("start"))
695 end = rmatch.group("end")
695 end = rmatch.group("end")
696 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
696 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
697 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
697 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
698 end += 1
698 end += 1
699 startsess = rmatch.group("startsess") or "0"
699 startsess = rmatch.group("startsess") or "0"
700 endsess = rmatch.group("endsess") or startsess
700 endsess = rmatch.group("endsess") or startsess
701 startsess = int(startsess.replace("~","-"))
701 startsess = int(startsess.replace("~","-"))
702 endsess = int(endsess.replace("~","-"))
702 endsess = int(endsess.replace("~","-"))
703 assert endsess >= startsess
703 assert endsess >= startsess
704
704
705 if endsess == startsess:
705 if endsess == startsess:
706 yield (startsess, start, end)
706 yield (startsess, start, end)
707 continue
707 continue
708 # Multiple sessions in one range:
708 # Multiple sessions in one range:
709 yield (startsess, start, None)
709 yield (startsess, start, None)
710 for sess in range(startsess+1, endsess):
710 for sess in range(startsess+1, endsess):
711 yield (sess, 1, None)
711 yield (sess, 1, None)
712 yield (endsess, 1, end)
712 yield (endsess, 1, end)
713
713
714
714
715 def _format_lineno(session, line):
715 def _format_lineno(session, line):
716 """Helper function to format line numbers properly."""
716 """Helper function to format line numbers properly."""
717 if session == 0:
717 if session == 0:
718 return str(line)
718 return str(line)
719 return "%s#%s" % (session, line)
719 return "%s#%s" % (session, line)
720
720
721
721
General Comments 0
You need to be logged in to leave comments. Login now