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