##// END OF EJS Templates
joined two message lines to one
Joon Ro -
Show More
@@ -1,798 +1,797 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.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'', config=None, **traits):
151 def __init__(self, profile='default', hist_file=u'', config=None, **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__(config=config, **traits)
165 super(HistoryAccessor, self).__init__(config=config, **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 self.shell.push(to_main, interactive=False)
638 self.shell.push(to_main, interactive=False)
639
639
640 def store_output(self, line_num):
640 def store_output(self, line_num):
641 """If database output logging is enabled, this saves all the
641 """If database output logging is enabled, this saves all the
642 outputs from the indicated prompt number to the database. It's
642 outputs from the indicated prompt number to the database. It's
643 called by run_cell after code has been executed.
643 called by run_cell after code has been executed.
644
644
645 Parameters
645 Parameters
646 ----------
646 ----------
647 line_num : int
647 line_num : int
648 The line number from which to save outputs
648 The line number from which to save outputs
649 """
649 """
650 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
650 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
651 return
651 return
652 output = self.output_hist_reprs[line_num]
652 output = self.output_hist_reprs[line_num]
653
653
654 with self.db_output_cache_lock:
654 with self.db_output_cache_lock:
655 self.db_output_cache.append((line_num, output))
655 self.db_output_cache.append((line_num, output))
656 if self.db_cache_size <= 1:
656 if self.db_cache_size <= 1:
657 self.save_flag.set()
657 self.save_flag.set()
658
658
659 def _writeout_input_cache(self, conn):
659 def _writeout_input_cache(self, conn):
660 with conn:
660 with conn:
661 for line in self.db_input_cache:
661 for line in self.db_input_cache:
662 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
662 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
663 (self.session_number,)+line)
663 (self.session_number,)+line)
664
664
665 def _writeout_output_cache(self, conn):
665 def _writeout_output_cache(self, conn):
666 with conn:
666 with conn:
667 for line in self.db_output_cache:
667 for line in self.db_output_cache:
668 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
668 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
669 (self.session_number,)+line)
669 (self.session_number,)+line)
670
670
671 @needs_sqlite
671 @needs_sqlite
672 def writeout_cache(self, conn=None):
672 def writeout_cache(self, conn=None):
673 """Write any entries in the cache to the database."""
673 """Write any entries in the cache to the database."""
674 if conn is None:
674 if conn is None:
675 conn = self.db
675 conn = self.db
676
676
677 with self.db_input_cache_lock:
677 with self.db_input_cache_lock:
678 try:
678 try:
679 self._writeout_input_cache(conn)
679 self._writeout_input_cache(conn)
680 except sqlite3.IntegrityError:
680 except sqlite3.IntegrityError:
681 self.new_session(conn)
681 self.new_session(conn)
682 print("ERROR! Session/line number was not unique in",
682 print("ERROR! Session/line number was not unique in",
683 "database. History logging moved to new session",
683 "database. History logging moved to new session",
684 self.session_number)
684 self.session_number)
685 try:
685 try:
686 # Try writing to the new session. If this fails, don't
686 # Try writing to the new session. If this fails, don't
687 # recurse
687 # recurse
688 self._writeout_input_cache(conn)
688 self._writeout_input_cache(conn)
689 except sqlite3.IntegrityError:
689 except sqlite3.IntegrityError:
690 pass
690 pass
691 finally:
691 finally:
692 self.db_input_cache = []
692 self.db_input_cache = []
693
693
694 with self.db_output_cache_lock:
694 with self.db_output_cache_lock:
695 try:
695 try:
696 self._writeout_output_cache(conn)
696 self._writeout_output_cache(conn)
697 except sqlite3.IntegrityError:
697 except sqlite3.IntegrityError:
698 print("!! Session/line number for output was not unique",
698 print("!! Session/line number for output was not unique",
699 "in database. Output will not be stored.")
699 "in database. Output will not be stored.")
700 finally:
700 finally:
701 self.db_output_cache = []
701 self.db_output_cache = []
702
702
703
703
704 class HistorySavingThread(threading.Thread):
704 class HistorySavingThread(threading.Thread):
705 """This thread takes care of writing history to the database, so that
705 """This thread takes care of writing history to the database, so that
706 the UI isn't held up while that happens.
706 the UI isn't held up while that happens.
707
707
708 It waits for the HistoryManager's save_flag to be set, then writes out
708 It waits for the HistoryManager's save_flag to be set, then writes out
709 the history cache. The main thread is responsible for setting the flag when
709 the history cache. The main thread is responsible for setting the flag when
710 the cache size reaches a defined threshold."""
710 the cache size reaches a defined threshold."""
711 daemon = True
711 daemon = True
712 stop_now = False
712 stop_now = False
713 enabled = True
713 enabled = True
714 def __init__(self, history_manager):
714 def __init__(self, history_manager):
715 super(HistorySavingThread, self).__init__()
715 super(HistorySavingThread, self).__init__()
716 self.history_manager = history_manager
716 self.history_manager = history_manager
717 self.enabled = history_manager.enabled
717 self.enabled = history_manager.enabled
718 atexit.register(self.stop)
718 atexit.register(self.stop)
719
719
720 @needs_sqlite
720 @needs_sqlite
721 def run(self):
721 def run(self):
722 # We need a separate db connection per thread:
722 # We need a separate db connection per thread:
723 try:
723 try:
724 self.db = sqlite3.connect(self.history_manager.hist_file,
724 self.db = sqlite3.connect(self.history_manager.hist_file,
725 **self.history_manager.connection_options
725 **self.history_manager.connection_options
726 )
726 )
727 while True:
727 while True:
728 self.history_manager.save_flag.wait()
728 self.history_manager.save_flag.wait()
729 if self.stop_now:
729 if self.stop_now:
730 return
730 return
731 self.history_manager.save_flag.clear()
731 self.history_manager.save_flag.clear()
732 self.history_manager.writeout_cache(self.db)
732 self.history_manager.writeout_cache(self.db)
733 except Exception as e:
733 except Exception as e:
734 print(("The history saving thread hit an unexpected error (%s)."
734 print(("The history saving thread hit an unexpected error (%s)."
735 "History will not be written to the database.") % repr(e))
735 "History will not be written to the database.") % repr(e))
736
736
737 def stop(self):
737 def stop(self):
738 """This can be called from the main thread to safely stop this thread.
738 """This can be called from the main thread to safely stop this thread.
739
739
740 Note that it does not attempt to write out remaining history before
740 Note that it does not attempt to write out remaining history before
741 exiting. That should be done by calling the HistoryManager's
741 exiting. That should be done by calling the HistoryManager's
742 end_session method."""
742 end_session method."""
743 self.stop_now = True
743 self.stop_now = True
744 self.history_manager.save_flag.set()
744 self.history_manager.save_flag.set()
745 self.join()
745 self.join()
746
746
747
747
748 # To match, e.g. ~5/8-~2/3
748 # To match, e.g. ~5/8-~2/3
749 range_re = re.compile(r"""
749 range_re = re.compile(r"""
750 ((?P<startsess>~?\d+)/)?
750 ((?P<startsess>~?\d+)/)?
751 (?P<start>\d+) # Only the start line num is compulsory
751 (?P<start>\d+) # Only the start line num is compulsory
752 ((?P<sep>[\-:])
752 ((?P<sep>[\-:])
753 ((?P<endsess>~?\d+)/)?
753 ((?P<endsess>~?\d+)/)?
754 (?P<end>\d+))?
754 (?P<end>\d+))?
755 $""", re.VERBOSE)
755 $""", re.VERBOSE)
756
756
757
757
758 def extract_hist_ranges(ranges_str):
758 def extract_hist_ranges(ranges_str):
759 """Turn a string of history ranges into 3-tuples of (session, start, stop).
759 """Turn a string of history ranges into 3-tuples of (session, start, stop).
760
760
761 Examples
761 Examples
762 --------
762 --------
763 list(extract_input_ranges("~8/5-~7/4 2"))
763 list(extract_input_ranges("~8/5-~7/4 2"))
764 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
764 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
765 """
765 """
766 for range_str in ranges_str.split():
766 for range_str in ranges_str.split():
767 rmatch = range_re.match(range_str)
767 rmatch = range_re.match(range_str)
768 if not rmatch:
768 if not rmatch:
769 continue
769 continue
770 start = int(rmatch.group("start"))
770 start = int(rmatch.group("start"))
771 end = rmatch.group("end")
771 end = rmatch.group("end")
772 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
772 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
773 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
773 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
774 end += 1
774 end += 1
775 startsess = rmatch.group("startsess") or "0"
775 startsess = rmatch.group("startsess") or "0"
776 endsess = rmatch.group("endsess") or startsess
776 endsess = rmatch.group("endsess") or startsess
777 startsess = int(startsess.replace("~","-"))
777 startsess = int(startsess.replace("~","-"))
778 endsess = int(endsess.replace("~","-"))
778 endsess = int(endsess.replace("~","-"))
779 assert endsess >= startsess, ("start session must be older than end "
779 assert endsess >= startsess, "start session must be earlier than end session"
780 "session")
781
780
782 if endsess == startsess:
781 if endsess == startsess:
783 yield (startsess, start, end)
782 yield (startsess, start, end)
784 continue
783 continue
785 # Multiple sessions in one range:
784 # Multiple sessions in one range:
786 yield (startsess, start, None)
785 yield (startsess, start, None)
787 for sess in range(startsess+1, endsess):
786 for sess in range(startsess+1, endsess):
788 yield (sess, 1, None)
787 yield (sess, 1, None)
789 yield (endsess, 1, end)
788 yield (endsess, 1, end)
790
789
791
790
792 def _format_lineno(session, line):
791 def _format_lineno(session, line):
793 """Helper function to format line numbers properly."""
792 """Helper function to format line numbers properly."""
794 if session == 0:
793 if session == 0:
795 return str(line)
794 return str(line)
796 return "%s#%s" % (session, line)
795 return "%s#%s" % (session, line)
797
796
798
797
General Comments 0
You need to be logged in to leave comments. Login now