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