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