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