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