##// END OF EJS Templates
Move error catching for corrupt database - error wasn't actually raised in _run_sql()
Thomas Kluyver -
Show More
@@ -1,786 +1,788 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 @catch_corrupt_db
225 def _run_sql(self, sql, params, raw=True, output=False):
224 def _run_sql(self, sql, params, raw=True, output=False):
226 """Prepares and runs an SQL query for the history database.
225 """Prepares and runs an SQL query for the history database.
227
226
228 Parameters
227 Parameters
229 ----------
228 ----------
230 sql : str
229 sql : str
231 Any filtering expressions to go after SELECT ... FROM ...
230 Any filtering expressions to go after SELECT ... FROM ...
232 params : tuple
231 params : tuple
233 Parameters passed to the SQL query (to replace "?")
232 Parameters passed to the SQL query (to replace "?")
234 raw, output : bool
233 raw, output : bool
235 See :meth:`get_range`
234 See :meth:`get_range`
236
235
237 Returns
236 Returns
238 -------
237 -------
239 Tuples as :meth:`get_range`
238 Tuples as :meth:`get_range`
240 """
239 """
241 toget = 'source_raw' if raw else 'source'
240 toget = 'source_raw' if raw else 'source'
242 sqlfrom = "history"
241 sqlfrom = "history"
243 if output:
242 if output:
244 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
243 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
245 toget = "history.%s, output_history.output" % toget
244 toget = "history.%s, output_history.output" % toget
246 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
245 cur = self.db.execute("SELECT session, line, %s FROM %s " %\
247 (toget, sqlfrom) + sql, params)
246 (toget, sqlfrom) + sql, params)
248 if output: # Regroup into 3-tuples, and parse JSON
247 if output: # Regroup into 3-tuples, and parse JSON
249 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)
250 return cur
249 return cur
251
250
252 @needs_sqlite
251 @needs_sqlite
253 @catch_corrupt_db
252 @catch_corrupt_db
254 def get_session_info(self, session=0):
253 def get_session_info(self, session=0):
255 """get info about a session
254 """get info about a session
256
255
257 Parameters
256 Parameters
258 ----------
257 ----------
259
258
260 session : int
259 session : int
261 Session number to retrieve. The current session is 0, and negative
260 Session number to retrieve. The current session is 0, and negative
262 numbers count back from current session, so -1 is previous session.
261 numbers count back from current session, so -1 is previous session.
263
262
264 Returns
263 Returns
265 -------
264 -------
266
265
267 (session_id [int], start [datetime], end [datetime], num_cmds [int],
266 (session_id [int], start [datetime], end [datetime], num_cmds [int],
268 remark [unicode])
267 remark [unicode])
269
268
270 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`
271 and `num_cmds=None`.
270 and `num_cmds=None`.
272
271
273 """
272 """
274
273
275 if session <= 0:
274 if session <= 0:
276 session += self.session_number
275 session += self.session_number
277
276
278 query = "SELECT * from sessions where session == ?"
277 query = "SELECT * from sessions where session == ?"
279 return self.db.execute(query, (session,)).fetchone()
278 return self.db.execute(query, (session,)).fetchone()
280
279
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 def search(self, pattern="*", raw=True, search_raw=True,
309 def search(self, pattern="*", raw=True, search_raw=True,
309 output=False, n=None):
310 output=False, n=None):
310 """Search the database using unix glob-style matching (wildcards
311 """Search the database using unix glob-style matching (wildcards
311 * and ?).
312 * and ?).
312
313
313 Parameters
314 Parameters
314 ----------
315 ----------
315 pattern : str
316 pattern : str
316 The wildcarded pattern to match when searching
317 The wildcarded pattern to match when searching
317 search_raw : bool
318 search_raw : bool
318 If True, search the raw input, otherwise, the parsed input
319 If True, search the raw input, otherwise, the parsed input
319 raw, output : bool
320 raw, output : bool
320 See :meth:`get_range`
321 See :meth:`get_range`
321 n : None or int
322 n : None or int
322 If an integer is given, it defines the limit of
323 If an integer is given, it defines the limit of
323 returned entries.
324 returned entries.
324
325
325 Returns
326 Returns
326 -------
327 -------
327 Tuples as :meth:`get_range`
328 Tuples as :meth:`get_range`
328 """
329 """
329 tosearch = "source_raw" if search_raw else "source"
330 tosearch = "source_raw" if search_raw else "source"
330 if output:
331 if output:
331 tosearch = "history." + tosearch
332 tosearch = "history." + tosearch
332 self.writeout_cache()
333 self.writeout_cache()
333 sqlform = "WHERE %s GLOB ?" % tosearch
334 sqlform = "WHERE %s GLOB ?" % tosearch
334 params = (pattern,)
335 params = (pattern,)
335 if n is not None:
336 if n is not None:
336 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
337 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
337 params += (n,)
338 params += (n,)
338 cur = self._run_sql(sqlform, params, raw=raw, output=output)
339 cur = self._run_sql(sqlform, params, raw=raw, output=output)
339 if n is not None:
340 if n is not None:
340 return reversed(list(cur))
341 return reversed(list(cur))
341 return cur
342 return cur
342
343
344 @catch_corrupt_db
343 def get_range(self, session, start=1, stop=None, raw=True,output=False):
345 def get_range(self, session, start=1, stop=None, raw=True,output=False):
344 """Retrieve input by session.
346 """Retrieve input by session.
345
347
346 Parameters
348 Parameters
347 ----------
349 ----------
348 session : int
350 session : int
349 Session number to retrieve.
351 Session number to retrieve.
350 start : int
352 start : int
351 First line to retrieve.
353 First line to retrieve.
352 stop : int
354 stop : int
353 End of line range (excluded from output itself). If None, retrieve
355 End of line range (excluded from output itself). If None, retrieve
354 to the end of the session.
356 to the end of the session.
355 raw : bool
357 raw : bool
356 If True, return untranslated input
358 If True, return untranslated input
357 output : bool
359 output : bool
358 If True, attempt to include output. This will be 'real' Python
360 If True, attempt to include output. This will be 'real' Python
359 objects for the current session, or text reprs from previous
361 objects for the current session, or text reprs from previous
360 sessions if db_log_output was enabled at the time. Where no output
362 sessions if db_log_output was enabled at the time. Where no output
361 is found, None is used.
363 is found, None is used.
362
364
363 Returns
365 Returns
364 -------
366 -------
365 An iterator over the desired lines. Each line is a 3-tuple, either
367 An iterator over the desired lines. Each line is a 3-tuple, either
366 (session, line, input) if output is False, or
368 (session, line, input) if output is False, or
367 (session, line, (input, output)) if output is True.
369 (session, line, (input, output)) if output is True.
368 """
370 """
369 if stop:
371 if stop:
370 lineclause = "line >= ? AND line < ?"
372 lineclause = "line >= ? AND line < ?"
371 params = (session, start, stop)
373 params = (session, start, stop)
372 else:
374 else:
373 lineclause = "line>=?"
375 lineclause = "line>=?"
374 params = (session, start)
376 params = (session, start)
375
377
376 return self._run_sql("WHERE session==? AND %s" % lineclause,
378 return self._run_sql("WHERE session==? AND %s" % lineclause,
377 params, raw=raw, output=output)
379 params, raw=raw, output=output)
378
380
379 def get_range_by_str(self, rangestr, raw=True, output=False):
381 def get_range_by_str(self, rangestr, raw=True, output=False):
380 """Get lines of history from a string of ranges, as used by magic
382 """Get lines of history from a string of ranges, as used by magic
381 commands %hist, %save, %macro, etc.
383 commands %hist, %save, %macro, etc.
382
384
383 Parameters
385 Parameters
384 ----------
386 ----------
385 rangestr : str
387 rangestr : str
386 A string specifying ranges, e.g. "5 ~2/1-4". See
388 A string specifying ranges, e.g. "5 ~2/1-4". See
387 :func:`magic_history` for full details.
389 :func:`magic_history` for full details.
388 raw, output : bool
390 raw, output : bool
389 As :meth:`get_range`
391 As :meth:`get_range`
390
392
391 Returns
393 Returns
392 -------
394 -------
393 Tuples as :meth:`get_range`
395 Tuples as :meth:`get_range`
394 """
396 """
395 for sess, s, e in extract_hist_ranges(rangestr):
397 for sess, s, e in extract_hist_ranges(rangestr):
396 for line in self.get_range(sess, s, e, raw=raw, output=output):
398 for line in self.get_range(sess, s, e, raw=raw, output=output):
397 yield line
399 yield line
398
400
399
401
400 class HistoryManager(HistoryAccessor):
402 class HistoryManager(HistoryAccessor):
401 """A class to organize all history-related functionality in one place.
403 """A class to organize all history-related functionality in one place.
402 """
404 """
403 # Public interface
405 # Public interface
404
406
405 # An instance of the IPython shell we are attached to
407 # An instance of the IPython shell we are attached to
406 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
408 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
407 # Lists to hold processed and raw history. These start with a blank entry
409 # Lists to hold processed and raw history. These start with a blank entry
408 # so that we can index them starting from 1
410 # so that we can index them starting from 1
409 input_hist_parsed = List([""])
411 input_hist_parsed = List([""])
410 input_hist_raw = List([""])
412 input_hist_raw = List([""])
411 # A list of directories visited during session
413 # A list of directories visited during session
412 dir_hist = List()
414 dir_hist = List()
413 def _dir_hist_default(self):
415 def _dir_hist_default(self):
414 try:
416 try:
415 return [os.getcwdu()]
417 return [os.getcwdu()]
416 except OSError:
418 except OSError:
417 return []
419 return []
418
420
419 # A dict of output history, keyed with ints from the shell's
421 # A dict of output history, keyed with ints from the shell's
420 # execution count.
422 # execution count.
421 output_hist = Dict()
423 output_hist = Dict()
422 # The text/plain repr of outputs.
424 # The text/plain repr of outputs.
423 output_hist_reprs = Dict()
425 output_hist_reprs = Dict()
424
426
425 # The number of the current session in the history database
427 # The number of the current session in the history database
426 session_number = Integer()
428 session_number = Integer()
427 # Should we log output to the database? (default no)
429 # Should we log output to the database? (default no)
428 db_log_output = Bool(False, config=True)
430 db_log_output = Bool(False, config=True)
429 # Write to database every x commands (higher values save disk access & power)
431 # Write to database every x commands (higher values save disk access & power)
430 # Values of 1 or less effectively disable caching.
432 # Values of 1 or less effectively disable caching.
431 db_cache_size = Integer(0, config=True)
433 db_cache_size = Integer(0, config=True)
432 # The input and output caches
434 # The input and output caches
433 db_input_cache = List()
435 db_input_cache = List()
434 db_output_cache = List()
436 db_output_cache = List()
435
437
436 # History saving in separate thread
438 # History saving in separate thread
437 save_thread = Instance('IPython.core.history.HistorySavingThread')
439 save_thread = Instance('IPython.core.history.HistorySavingThread')
438 try: # Event is a function returning an instance of _Event...
440 try: # Event is a function returning an instance of _Event...
439 save_flag = Instance(threading._Event)
441 save_flag = Instance(threading._Event)
440 except AttributeError: # ...until Python 3.3, when it's a class.
442 except AttributeError: # ...until Python 3.3, when it's a class.
441 save_flag = Instance(threading.Event)
443 save_flag = Instance(threading.Event)
442
444
443 # Private interface
445 # Private interface
444 # Variables used to store the three last inputs from the user. On each new
446 # Variables used to store the three last inputs from the user. On each new
445 # history update, we populate the user's namespace with these, shifted as
447 # history update, we populate the user's namespace with these, shifted as
446 # necessary.
448 # necessary.
447 _i00 = Unicode(u'')
449 _i00 = Unicode(u'')
448 _i = Unicode(u'')
450 _i = Unicode(u'')
449 _ii = Unicode(u'')
451 _ii = Unicode(u'')
450 _iii = Unicode(u'')
452 _iii = Unicode(u'')
451
453
452 # A regex matching all forms of the exit command, so that we don't store
454 # A regex matching all forms of the exit command, so that we don't store
453 # them in the history (it's annoying to rewind the first entry and land on
455 # them in the history (it's annoying to rewind the first entry and land on
454 # an exit call).
456 # an exit call).
455 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
457 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
456
458
457 def __init__(self, shell=None, config=None, **traits):
459 def __init__(self, shell=None, config=None, **traits):
458 """Create a new history manager associated with a shell instance.
460 """Create a new history manager associated with a shell instance.
459 """
461 """
460 # We need a pointer back to the shell for various tasks.
462 # We need a pointer back to the shell for various tasks.
461 super(HistoryManager, self).__init__(shell=shell, config=config,
463 super(HistoryManager, self).__init__(shell=shell, config=config,
462 **traits)
464 **traits)
463 self.save_flag = threading.Event()
465 self.save_flag = threading.Event()
464 self.db_input_cache_lock = threading.Lock()
466 self.db_input_cache_lock = threading.Lock()
465 self.db_output_cache_lock = threading.Lock()
467 self.db_output_cache_lock = threading.Lock()
466 if self.enabled and self.hist_file != ':memory:':
468 if self.enabled and self.hist_file != ':memory:':
467 self.save_thread = HistorySavingThread(self)
469 self.save_thread = HistorySavingThread(self)
468 self.save_thread.start()
470 self.save_thread.start()
469
471
470 self.new_session()
472 self.new_session()
471
473
472 def _get_hist_file_name(self, profile=None):
474 def _get_hist_file_name(self, profile=None):
473 """Get default history file name based on the Shell's profile.
475 """Get default history file name based on the Shell's profile.
474
476
475 The profile parameter is ignored, but must exist for compatibility with
477 The profile parameter is ignored, but must exist for compatibility with
476 the parent class."""
478 the parent class."""
477 profile_dir = self.shell.profile_dir.location
479 profile_dir = self.shell.profile_dir.location
478 return os.path.join(profile_dir, 'history.sqlite')
480 return os.path.join(profile_dir, 'history.sqlite')
479
481
480 @needs_sqlite
482 @needs_sqlite
481 def new_session(self, conn=None):
483 def new_session(self, conn=None):
482 """Get a new session number."""
484 """Get a new session number."""
483 if conn is None:
485 if conn is None:
484 conn = self.db
486 conn = self.db
485
487
486 with conn:
488 with conn:
487 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
489 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
488 NULL, "") """, (datetime.datetime.now(),))
490 NULL, "") """, (datetime.datetime.now(),))
489 self.session_number = cur.lastrowid
491 self.session_number = cur.lastrowid
490
492
491 def end_session(self):
493 def end_session(self):
492 """Close the database session, filling in the end time and line count."""
494 """Close the database session, filling in the end time and line count."""
493 self.writeout_cache()
495 self.writeout_cache()
494 with self.db:
496 with self.db:
495 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
497 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
496 session==?""", (datetime.datetime.now(),
498 session==?""", (datetime.datetime.now(),
497 len(self.input_hist_parsed)-1, self.session_number))
499 len(self.input_hist_parsed)-1, self.session_number))
498 self.session_number = 0
500 self.session_number = 0
499
501
500 def name_session(self, name):
502 def name_session(self, name):
501 """Give the current session a name in the history database."""
503 """Give the current session a name in the history database."""
502 with self.db:
504 with self.db:
503 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
505 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
504 (name, self.session_number))
506 (name, self.session_number))
505
507
506 def reset(self, new_session=True):
508 def reset(self, new_session=True):
507 """Clear the session history, releasing all object references, and
509 """Clear the session history, releasing all object references, and
508 optionally open a new session."""
510 optionally open a new session."""
509 self.output_hist.clear()
511 self.output_hist.clear()
510 # The directory history can't be completely empty
512 # The directory history can't be completely empty
511 self.dir_hist[:] = [os.getcwdu()]
513 self.dir_hist[:] = [os.getcwdu()]
512
514
513 if new_session:
515 if new_session:
514 if self.session_number:
516 if self.session_number:
515 self.end_session()
517 self.end_session()
516 self.input_hist_parsed[:] = [""]
518 self.input_hist_parsed[:] = [""]
517 self.input_hist_raw[:] = [""]
519 self.input_hist_raw[:] = [""]
518 self.new_session()
520 self.new_session()
519
521
520 # ------------------------------
522 # ------------------------------
521 # Methods for retrieving history
523 # Methods for retrieving history
522 # ------------------------------
524 # ------------------------------
523 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
525 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
524 """Get input and output history from the current session. Called by
526 """Get input and output history from the current session. Called by
525 get_range, and takes similar parameters."""
527 get_range, and takes similar parameters."""
526 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
528 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
527
529
528 n = len(input_hist)
530 n = len(input_hist)
529 if start < 0:
531 if start < 0:
530 start += n
532 start += n
531 if not stop or (stop > n):
533 if not stop or (stop > n):
532 stop = n
534 stop = n
533 elif stop < 0:
535 elif stop < 0:
534 stop += n
536 stop += n
535
537
536 for i in range(start, stop):
538 for i in range(start, stop):
537 if output:
539 if output:
538 line = (input_hist[i], self.output_hist_reprs.get(i))
540 line = (input_hist[i], self.output_hist_reprs.get(i))
539 else:
541 else:
540 line = input_hist[i]
542 line = input_hist[i]
541 yield (0, i, line)
543 yield (0, i, line)
542
544
543 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
545 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
544 """Retrieve input by session.
546 """Retrieve input by session.
545
547
546 Parameters
548 Parameters
547 ----------
549 ----------
548 session : int
550 session : int
549 Session number to retrieve. The current session is 0, and negative
551 Session number to retrieve. The current session is 0, and negative
550 numbers count back from current session, so -1 is previous session.
552 numbers count back from current session, so -1 is previous session.
551 start : int
553 start : int
552 First line to retrieve.
554 First line to retrieve.
553 stop : int
555 stop : int
554 End of line range (excluded from output itself). If None, retrieve
556 End of line range (excluded from output itself). If None, retrieve
555 to the end of the session.
557 to the end of the session.
556 raw : bool
558 raw : bool
557 If True, return untranslated input
559 If True, return untranslated input
558 output : bool
560 output : bool
559 If True, attempt to include output. This will be 'real' Python
561 If True, attempt to include output. This will be 'real' Python
560 objects for the current session, or text reprs from previous
562 objects for the current session, or text reprs from previous
561 sessions if db_log_output was enabled at the time. Where no output
563 sessions if db_log_output was enabled at the time. Where no output
562 is found, None is used.
564 is found, None is used.
563
565
564 Returns
566 Returns
565 -------
567 -------
566 An iterator over the desired lines. Each line is a 3-tuple, either
568 An iterator over the desired lines. Each line is a 3-tuple, either
567 (session, line, input) if output is False, or
569 (session, line, input) if output is False, or
568 (session, line, (input, output)) if output is True.
570 (session, line, (input, output)) if output is True.
569 """
571 """
570 if session <= 0:
572 if session <= 0:
571 session += self.session_number
573 session += self.session_number
572 if session==self.session_number: # Current session
574 if session==self.session_number: # Current session
573 return self._get_range_session(start, stop, raw, output)
575 return self._get_range_session(start, stop, raw, output)
574 return super(HistoryManager, self).get_range(session, start, stop, raw,
576 return super(HistoryManager, self).get_range(session, start, stop, raw,
575 output)
577 output)
576
578
577 ## ----------------------------
579 ## ----------------------------
578 ## Methods for storing history:
580 ## Methods for storing history:
579 ## ----------------------------
581 ## ----------------------------
580 def store_inputs(self, line_num, source, source_raw=None):
582 def store_inputs(self, line_num, source, source_raw=None):
581 """Store source and raw input in history and create input cache
583 """Store source and raw input in history and create input cache
582 variables _i*.
584 variables _i*.
583
585
584 Parameters
586 Parameters
585 ----------
587 ----------
586 line_num : int
588 line_num : int
587 The prompt number of this input.
589 The prompt number of this input.
588
590
589 source : str
591 source : str
590 Python input.
592 Python input.
591
593
592 source_raw : str, optional
594 source_raw : str, optional
593 If given, this is the raw input without any IPython transformations
595 If given, this is the raw input without any IPython transformations
594 applied to it. If not given, ``source`` is used.
596 applied to it. If not given, ``source`` is used.
595 """
597 """
596 if source_raw is None:
598 if source_raw is None:
597 source_raw = source
599 source_raw = source
598 source = source.rstrip('\n')
600 source = source.rstrip('\n')
599 source_raw = source_raw.rstrip('\n')
601 source_raw = source_raw.rstrip('\n')
600
602
601 # do not store exit/quit commands
603 # do not store exit/quit commands
602 if self._exit_re.match(source_raw.strip()):
604 if self._exit_re.match(source_raw.strip()):
603 return
605 return
604
606
605 self.input_hist_parsed.append(source)
607 self.input_hist_parsed.append(source)
606 self.input_hist_raw.append(source_raw)
608 self.input_hist_raw.append(source_raw)
607
609
608 with self.db_input_cache_lock:
610 with self.db_input_cache_lock:
609 self.db_input_cache.append((line_num, source, source_raw))
611 self.db_input_cache.append((line_num, source, source_raw))
610 # Trigger to flush cache and write to DB.
612 # Trigger to flush cache and write to DB.
611 if len(self.db_input_cache) >= self.db_cache_size:
613 if len(self.db_input_cache) >= self.db_cache_size:
612 self.save_flag.set()
614 self.save_flag.set()
613
615
614 # update the auto _i variables
616 # update the auto _i variables
615 self._iii = self._ii
617 self._iii = self._ii
616 self._ii = self._i
618 self._ii = self._i
617 self._i = self._i00
619 self._i = self._i00
618 self._i00 = source_raw
620 self._i00 = source_raw
619
621
620 # hackish access to user namespace to create _i1,_i2... dynamically
622 # hackish access to user namespace to create _i1,_i2... dynamically
621 new_i = '_i%s' % line_num
623 new_i = '_i%s' % line_num
622 to_main = {'_i': self._i,
624 to_main = {'_i': self._i,
623 '_ii': self._ii,
625 '_ii': self._ii,
624 '_iii': self._iii,
626 '_iii': self._iii,
625 new_i : self._i00 }
627 new_i : self._i00 }
626
628
627 self.shell.push(to_main, interactive=False)
629 self.shell.push(to_main, interactive=False)
628
630
629 def store_output(self, line_num):
631 def store_output(self, line_num):
630 """If database output logging is enabled, this saves all the
632 """If database output logging is enabled, this saves all the
631 outputs from the indicated prompt number to the database. It's
633 outputs from the indicated prompt number to the database. It's
632 called by run_cell after code has been executed.
634 called by run_cell after code has been executed.
633
635
634 Parameters
636 Parameters
635 ----------
637 ----------
636 line_num : int
638 line_num : int
637 The line number from which to save outputs
639 The line number from which to save outputs
638 """
640 """
639 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
641 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
640 return
642 return
641 output = self.output_hist_reprs[line_num]
643 output = self.output_hist_reprs[line_num]
642
644
643 with self.db_output_cache_lock:
645 with self.db_output_cache_lock:
644 self.db_output_cache.append((line_num, output))
646 self.db_output_cache.append((line_num, output))
645 if self.db_cache_size <= 1:
647 if self.db_cache_size <= 1:
646 self.save_flag.set()
648 self.save_flag.set()
647
649
648 def _writeout_input_cache(self, conn):
650 def _writeout_input_cache(self, conn):
649 with conn:
651 with conn:
650 for line in self.db_input_cache:
652 for line in self.db_input_cache:
651 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
653 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
652 (self.session_number,)+line)
654 (self.session_number,)+line)
653
655
654 def _writeout_output_cache(self, conn):
656 def _writeout_output_cache(self, conn):
655 with conn:
657 with conn:
656 for line in self.db_output_cache:
658 for line in self.db_output_cache:
657 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
659 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
658 (self.session_number,)+line)
660 (self.session_number,)+line)
659
661
660 @needs_sqlite
662 @needs_sqlite
661 def writeout_cache(self, conn=None):
663 def writeout_cache(self, conn=None):
662 """Write any entries in the cache to the database."""
664 """Write any entries in the cache to the database."""
663 if conn is None:
665 if conn is None:
664 conn = self.db
666 conn = self.db
665
667
666 with self.db_input_cache_lock:
668 with self.db_input_cache_lock:
667 try:
669 try:
668 self._writeout_input_cache(conn)
670 self._writeout_input_cache(conn)
669 except sqlite3.IntegrityError:
671 except sqlite3.IntegrityError:
670 self.new_session(conn)
672 self.new_session(conn)
671 print("ERROR! Session/line number was not unique in",
673 print("ERROR! Session/line number was not unique in",
672 "database. History logging moved to new session",
674 "database. History logging moved to new session",
673 self.session_number)
675 self.session_number)
674 try:
676 try:
675 # Try writing to the new session. If this fails, don't
677 # Try writing to the new session. If this fails, don't
676 # recurse
678 # recurse
677 self._writeout_input_cache(conn)
679 self._writeout_input_cache(conn)
678 except sqlite3.IntegrityError:
680 except sqlite3.IntegrityError:
679 pass
681 pass
680 finally:
682 finally:
681 self.db_input_cache = []
683 self.db_input_cache = []
682
684
683 with self.db_output_cache_lock:
685 with self.db_output_cache_lock:
684 try:
686 try:
685 self._writeout_output_cache(conn)
687 self._writeout_output_cache(conn)
686 except sqlite3.IntegrityError:
688 except sqlite3.IntegrityError:
687 print("!! Session/line number for output was not unique",
689 print("!! Session/line number for output was not unique",
688 "in database. Output will not be stored.")
690 "in database. Output will not be stored.")
689 finally:
691 finally:
690 self.db_output_cache = []
692 self.db_output_cache = []
691
693
692
694
693 class HistorySavingThread(threading.Thread):
695 class HistorySavingThread(threading.Thread):
694 """This thread takes care of writing history to the database, so that
696 """This thread takes care of writing history to the database, so that
695 the UI isn't held up while that happens.
697 the UI isn't held up while that happens.
696
698
697 It waits for the HistoryManager's save_flag to be set, then writes out
699 It waits for the HistoryManager's save_flag to be set, then writes out
698 the history cache. The main thread is responsible for setting the flag when
700 the history cache. The main thread is responsible for setting the flag when
699 the cache size reaches a defined threshold."""
701 the cache size reaches a defined threshold."""
700 daemon = True
702 daemon = True
701 stop_now = False
703 stop_now = False
702 enabled = True
704 enabled = True
703 def __init__(self, history_manager):
705 def __init__(self, history_manager):
704 super(HistorySavingThread, self).__init__()
706 super(HistorySavingThread, self).__init__()
705 self.history_manager = history_manager
707 self.history_manager = history_manager
706 self.enabled = history_manager.enabled
708 self.enabled = history_manager.enabled
707 atexit.register(self.stop)
709 atexit.register(self.stop)
708
710
709 @needs_sqlite
711 @needs_sqlite
710 def run(self):
712 def run(self):
711 # We need a separate db connection per thread:
713 # We need a separate db connection per thread:
712 try:
714 try:
713 self.db = sqlite3.connect(self.history_manager.hist_file,
715 self.db = sqlite3.connect(self.history_manager.hist_file,
714 **self.history_manager.connection_options
716 **self.history_manager.connection_options
715 )
717 )
716 while True:
718 while True:
717 self.history_manager.save_flag.wait()
719 self.history_manager.save_flag.wait()
718 if self.stop_now:
720 if self.stop_now:
719 return
721 return
720 self.history_manager.save_flag.clear()
722 self.history_manager.save_flag.clear()
721 self.history_manager.writeout_cache(self.db)
723 self.history_manager.writeout_cache(self.db)
722 except Exception as e:
724 except Exception as e:
723 print(("The history saving thread hit an unexpected error (%s)."
725 print(("The history saving thread hit an unexpected error (%s)."
724 "History will not be written to the database.") % repr(e))
726 "History will not be written to the database.") % repr(e))
725
727
726 def stop(self):
728 def stop(self):
727 """This can be called from the main thread to safely stop this thread.
729 """This can be called from the main thread to safely stop this thread.
728
730
729 Note that it does not attempt to write out remaining history before
731 Note that it does not attempt to write out remaining history before
730 exiting. That should be done by calling the HistoryManager's
732 exiting. That should be done by calling the HistoryManager's
731 end_session method."""
733 end_session method."""
732 self.stop_now = True
734 self.stop_now = True
733 self.history_manager.save_flag.set()
735 self.history_manager.save_flag.set()
734 self.join()
736 self.join()
735
737
736
738
737 # To match, e.g. ~5/8-~2/3
739 # To match, e.g. ~5/8-~2/3
738 range_re = re.compile(r"""
740 range_re = re.compile(r"""
739 ((?P<startsess>~?\d+)/)?
741 ((?P<startsess>~?\d+)/)?
740 (?P<start>\d+) # Only the start line num is compulsory
742 (?P<start>\d+) # Only the start line num is compulsory
741 ((?P<sep>[\-:])
743 ((?P<sep>[\-:])
742 ((?P<endsess>~?\d+)/)?
744 ((?P<endsess>~?\d+)/)?
743 (?P<end>\d+))?
745 (?P<end>\d+))?
744 $""", re.VERBOSE)
746 $""", re.VERBOSE)
745
747
746
748
747 def extract_hist_ranges(ranges_str):
749 def extract_hist_ranges(ranges_str):
748 """Turn a string of history ranges into 3-tuples of (session, start, stop).
750 """Turn a string of history ranges into 3-tuples of (session, start, stop).
749
751
750 Examples
752 Examples
751 --------
753 --------
752 list(extract_input_ranges("~8/5-~7/4 2"))
754 list(extract_input_ranges("~8/5-~7/4 2"))
753 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
755 [(-8, 5, None), (-7, 1, 4), (0, 2, 3)]
754 """
756 """
755 for range_str in ranges_str.split():
757 for range_str in ranges_str.split():
756 rmatch = range_re.match(range_str)
758 rmatch = range_re.match(range_str)
757 if not rmatch:
759 if not rmatch:
758 continue
760 continue
759 start = int(rmatch.group("start"))
761 start = int(rmatch.group("start"))
760 end = rmatch.group("end")
762 end = rmatch.group("end")
761 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
763 end = int(end) if end else start+1 # If no end specified, get (a, a+1)
762 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
764 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
763 end += 1
765 end += 1
764 startsess = rmatch.group("startsess") or "0"
766 startsess = rmatch.group("startsess") or "0"
765 endsess = rmatch.group("endsess") or startsess
767 endsess = rmatch.group("endsess") or startsess
766 startsess = int(startsess.replace("~","-"))
768 startsess = int(startsess.replace("~","-"))
767 endsess = int(endsess.replace("~","-"))
769 endsess = int(endsess.replace("~","-"))
768 assert endsess >= startsess
770 assert endsess >= startsess
769
771
770 if endsess == startsess:
772 if endsess == startsess:
771 yield (startsess, start, end)
773 yield (startsess, start, end)
772 continue
774 continue
773 # Multiple sessions in one range:
775 # Multiple sessions in one range:
774 yield (startsess, start, None)
776 yield (startsess, start, None)
775 for sess in range(startsess+1, endsess):
777 for sess in range(startsess+1, endsess):
776 yield (sess, 1, None)
778 yield (sess, 1, None)
777 yield (endsess, 1, end)
779 yield (endsess, 1, end)
778
780
779
781
780 def _format_lineno(session, line):
782 def _format_lineno(session, line):
781 """Helper function to format line numbers properly."""
783 """Helper function to format line numbers properly."""
782 if session == 0:
784 if session == 0:
783 return str(line)
785 return str(line)
784 return "%s#%s" % (session, line)
786 return "%s#%s" % (session, line)
785
787
786
788
General Comments 0
You need to be logged in to leave comments. Login now