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