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