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