##// END OF EJS Templates
Fix formatting issues
Aleksey Bogdanov -
Show More
@@ -1,964 +1,965 b''
1 """ History related magics and functionality """
1 """ History related magics and functionality """
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6
6
7 import atexit
7 import atexit
8 import datetime
8 import datetime
9 from pathlib import Path
9 from pathlib import Path
10 import re
10 import re
11 import sqlite3
11 import sqlite3
12 import threading
12 import threading
13
13
14 from traitlets.config.configurable import LoggingConfigurable
14 from traitlets.config.configurable import LoggingConfigurable
15 from decorator import decorator
15 from decorator import decorator
16 from IPython.utils.decorators import undoc
16 from IPython.utils.decorators import undoc
17 from IPython.paths import locate_profile
17 from IPython.paths import locate_profile
18 from traitlets import (
18 from traitlets import (
19 Any,
19 Any,
20 Bool,
20 Bool,
21 Dict,
21 Dict,
22 Instance,
22 Instance,
23 Integer,
23 Integer,
24 List,
24 List,
25 Unicode,
25 Unicode,
26 Union,
26 Union,
27 TraitError,
27 TraitError,
28 default,
28 default,
29 observe,
29 observe,
30 )
30 )
31
31
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33 # Classes and functions
33 # Classes and functions
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35
35
36 @undoc
36 @undoc
37 class DummyDB(object):
37 class DummyDB(object):
38 """Dummy DB that will act as a black hole for history.
38 """Dummy DB that will act as a black hole for history.
39
39
40 Only used in the absence of sqlite"""
40 Only used in the absence of sqlite"""
41 def execute(*args, **kwargs):
41 def execute(*args, **kwargs):
42 return []
42 return []
43
43
44 def commit(self, *args, **kwargs):
44 def commit(self, *args, **kwargs):
45 pass
45 pass
46
46
47 def __enter__(self, *args, **kwargs):
47 def __enter__(self, *args, **kwargs):
48 pass
48 pass
49
49
50 def __exit__(self, *args, **kwargs):
50 def __exit__(self, *args, **kwargs):
51 pass
51 pass
52
52
53
53
54 @decorator
54 @decorator
55 def only_when_enabled(f, self, *a, **kw):
55 def only_when_enabled(f, self, *a, **kw):
56 """Decorator: return an empty list in the absence of sqlite."""
56 """Decorator: return an empty list in the absence of sqlite."""
57 if not self.enabled:
57 if not self.enabled:
58 return []
58 return []
59 else:
59 else:
60 return f(self, *a, **kw)
60 return f(self, *a, **kw)
61
61
62
62
63 # use 16kB as threshold for whether a corrupt history db should be saved
63 # use 16kB as threshold for whether a corrupt history db should be saved
64 # that should be at least 100 entries or so
64 # that should be at least 100 entries or so
65 _SAVE_DB_SIZE = 16384
65 _SAVE_DB_SIZE = 16384
66
66
67 @decorator
67 @decorator
68 def catch_corrupt_db(f, self, *a, **kw):
68 def catch_corrupt_db(f, self, *a, **kw):
69 """A decorator which wraps HistoryAccessor method calls to catch errors from
69 """A decorator which wraps HistoryAccessor method calls to catch errors from
70 a corrupt SQLite database, move the old database out of the way, and create
70 a corrupt SQLite database, move the old database out of the way, and create
71 a new one.
71 a new one.
72
72
73 We avoid clobbering larger databases because this may be triggered due to filesystem issues,
73 We avoid clobbering larger databases because this may be triggered due to filesystem issues,
74 not just a corrupt file.
74 not just a corrupt file.
75 """
75 """
76 try:
76 try:
77 return f(self, *a, **kw)
77 return f(self, *a, **kw)
78 except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
78 except (sqlite3.DatabaseError, sqlite3.OperationalError) as e:
79 self._corrupt_db_counter += 1
79 self._corrupt_db_counter += 1
80 self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e)
80 self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e)
81 if self.hist_file != ':memory:':
81 if self.hist_file != ':memory:':
82 if self._corrupt_db_counter > self._corrupt_db_limit:
82 if self._corrupt_db_counter > self._corrupt_db_limit:
83 self.hist_file = ':memory:'
83 self.hist_file = ':memory:'
84 self.log.error("Failed to load history too many times, history will not be saved.")
84 self.log.error("Failed to load history too many times, history will not be saved.")
85 elif self.hist_file.is_file():
85 elif self.hist_file.is_file():
86 # move the file out of the way
86 # move the file out of the way
87 base = str(self.hist_file.parent / self.hist_file.stem)
87 base = str(self.hist_file.parent / self.hist_file.stem)
88 ext = self.hist_file.suffix
88 ext = self.hist_file.suffix
89 size = self.hist_file.stat().st_size
89 size = self.hist_file.stat().st_size
90 if size >= _SAVE_DB_SIZE:
90 if size >= _SAVE_DB_SIZE:
91 # if there's significant content, avoid clobbering
91 # if there's significant content, avoid clobbering
92 now = datetime.datetime.now().isoformat().replace(':', '.')
92 now = datetime.datetime.now().isoformat().replace(':', '.')
93 newpath = base + '-corrupt-' + now + ext
93 newpath = base + '-corrupt-' + now + ext
94 # don't clobber previous corrupt backups
94 # don't clobber previous corrupt backups
95 for i in range(100):
95 for i in range(100):
96 if not Path(newpath).exists():
96 if not Path(newpath).exists():
97 break
97 break
98 else:
98 else:
99 newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext
99 newpath = base + '-corrupt-' + now + (u'-%i' % i) + ext
100 else:
100 else:
101 # not much content, possibly empty; don't worry about clobbering
101 # not much content, possibly empty; don't worry about clobbering
102 # maybe we should just delete it?
102 # maybe we should just delete it?
103 newpath = base + '-corrupt' + ext
103 newpath = base + '-corrupt' + ext
104 self.hist_file.rename(newpath)
104 self.hist_file.rename(newpath)
105 self.log.error("History file was moved to %s and a new file created.", newpath)
105 self.log.error("History file was moved to %s and a new file created.", newpath)
106 self.init_db()
106 self.init_db()
107 return []
107 return []
108 else:
108 else:
109 # Failed with :memory:, something serious is wrong
109 # Failed with :memory:, something serious is wrong
110 raise
110 raise
111
111
112
112
113 class HistoryAccessorBase(LoggingConfigurable):
113 class HistoryAccessorBase(LoggingConfigurable):
114 """An abstract class for History Accessors """
114 """An abstract class for History Accessors """
115
115
116 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
116 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
117 raise NotImplementedError
117 raise NotImplementedError
118
118
119 def search(self, pattern="*", raw=True, search_raw=True,
119 def search(self, pattern="*", raw=True, search_raw=True,
120 output=False, n=None, unique=False):
120 output=False, n=None, unique=False):
121 raise NotImplementedError
121 raise NotImplementedError
122
122
123 def get_range(self, session, start=1, stop=None, raw=True,output=False):
123 def get_range(self, session, start=1, stop=None, raw=True,output=False):
124 raise NotImplementedError
124 raise NotImplementedError
125
125
126 def get_range_by_str(self, rangestr, raw=True, output=False):
126 def get_range_by_str(self, rangestr, raw=True, output=False):
127 raise NotImplementedError
127 raise NotImplementedError
128
128
129
129
130 class HistoryAccessor(HistoryAccessorBase):
130 class HistoryAccessor(HistoryAccessorBase):
131 """Access the history database without adding to it.
131 """Access the history database without adding to it.
132
132
133 This is intended for use by standalone history tools. IPython shells use
133 This is intended for use by standalone history tools. IPython shells use
134 HistoryManager, below, which is a subclass of this."""
134 HistoryManager, below, which is a subclass of this."""
135
135
136 # counter for init_db retries, so we don't keep trying over and over
136 # counter for init_db retries, so we don't keep trying over and over
137 _corrupt_db_counter = 0
137 _corrupt_db_counter = 0
138 # after two failures, fallback on :memory:
138 # after two failures, fallback on :memory:
139 _corrupt_db_limit = 2
139 _corrupt_db_limit = 2
140
140
141 # String holding the path to the history file
141 # String holding the path to the history file
142 hist_file = Union(
142 hist_file = Union(
143 [Instance(Path), Unicode()],
143 [Instance(Path), Unicode()],
144 help="""Path to file to use for SQLite history database.
144 help="""Path to file to use for SQLite history database.
145
145
146 By default, IPython will put the history database in the IPython
146 By default, IPython will put the history database in the IPython
147 profile directory. If you would rather share one history among
147 profile directory. If you would rather share one history among
148 profiles, you can set this value in each, so that they are consistent.
148 profiles, you can set this value in each, so that they are consistent.
149
149
150 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
150 Due to an issue with fcntl, SQLite is known to misbehave on some NFS
151 mounts. If you see IPython hanging, try setting this to something on a
151 mounts. If you see IPython hanging, try setting this to something on a
152 local disk, e.g::
152 local disk, e.g::
153
153
154 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
154 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite
155
155
156 you can also use the specific value `:memory:` (including the colon
156 you can also use the specific value `:memory:` (including the colon
157 at both end but not the back ticks), to avoid creating an history file.
157 at both end but not the back ticks), to avoid creating an history file.
158
158
159 """,
159 """,
160 ).tag(config=True)
160 ).tag(config=True)
161
161
162 enabled = Bool(True,
162 enabled = Bool(True,
163 help="""enable the SQLite history
163 help="""enable the SQLite history
164
164
165 set enabled=False to disable the SQLite history,
165 set enabled=False to disable the SQLite history,
166 in which case there will be no stored history, no SQLite connection,
166 in which case there will be no stored history, no SQLite connection,
167 and no background saving thread. This may be necessary in some
167 and no background saving thread. This may be necessary in some
168 threaded environments where IPython is embedded.
168 threaded environments where IPython is embedded.
169 """,
169 """,
170 ).tag(config=True)
170 ).tag(config=True)
171
171
172 connection_options = Dict(
172 connection_options = Dict(
173 help="""Options for configuring the SQLite connection
173 help="""Options for configuring the SQLite connection
174
174
175 These options are passed as keyword args to sqlite3.connect
175 These options are passed as keyword args to sqlite3.connect
176 when establishing database connections.
176 when establishing database connections.
177 """
177 """
178 ).tag(config=True)
178 ).tag(config=True)
179
179
180 # The SQLite database
180 # The SQLite database
181 db = Any()
181 db = Any()
182 @observe('db')
182 @observe('db')
183 def _db_changed(self, change):
183 def _db_changed(self, change):
184 """validate the db, since it can be an Instance of two different types"""
184 """validate the db, since it can be an Instance of two different types"""
185 new = change['new']
185 new = change['new']
186 connection_types = (DummyDB, sqlite3.Connection)
186 connection_types = (DummyDB, sqlite3.Connection)
187 if not isinstance(new, connection_types):
187 if not isinstance(new, connection_types):
188 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
188 msg = "%s.db must be sqlite3 Connection or DummyDB, not %r" % \
189 (self.__class__.__name__, new)
189 (self.__class__.__name__, new)
190 raise TraitError(msg)
190 raise TraitError(msg)
191
191
192 def __init__(self, profile="default", hist_file="", **traits):
192 def __init__(self, profile="default", hist_file="", **traits):
193 """Create a new history accessor.
193 """Create a new history accessor.
194
194
195 Parameters
195 Parameters
196 ----------
196 ----------
197 profile : str
197 profile : str
198 The name of the profile from which to open history.
198 The name of the profile from which to open history.
199 hist_file : str
199 hist_file : str
200 Path to an SQLite history database stored by IPython. If specified,
200 Path to an SQLite history database stored by IPython. If specified,
201 hist_file overrides profile.
201 hist_file overrides profile.
202 config : :class:`~traitlets.config.loader.Config`
202 config : :class:`~traitlets.config.loader.Config`
203 Config object. hist_file can also be set through this.
203 Config object. hist_file can also be set through this.
204 """
204 """
205 super(HistoryAccessor, self).__init__(**traits)
205 super(HistoryAccessor, self).__init__(**traits)
206 # defer setting hist_file from kwarg until after init,
206 # defer setting hist_file from kwarg until after init,
207 # otherwise the default kwarg value would clobber any value
207 # otherwise the default kwarg value would clobber any value
208 # set by config
208 # set by config
209 if hist_file:
209 if hist_file:
210 self.hist_file = hist_file
210 self.hist_file = hist_file
211
211
212 try:
212 try:
213 self.hist_file
213 self.hist_file
214 except TraitError:
214 except TraitError:
215 # No one has set the hist_file, yet.
215 # No one has set the hist_file, yet.
216 self.hist_file = self._get_hist_file_name(profile)
216 self.hist_file = self._get_hist_file_name(profile)
217
217
218 self.init_db()
218 self.init_db()
219
219
220 def _get_hist_file_name(self, profile='default'):
220 def _get_hist_file_name(self, profile='default'):
221 """Find the history file for the given profile name.
221 """Find the history file for the given profile name.
222
222
223 This is overridden by the HistoryManager subclass, to use the shell's
223 This is overridden by the HistoryManager subclass, to use the shell's
224 active profile.
224 active profile.
225
225
226 Parameters
226 Parameters
227 ----------
227 ----------
228 profile : str
228 profile : str
229 The name of a profile which has a history file.
229 The name of a profile which has a history file.
230 """
230 """
231 return Path(locate_profile(profile)) / "history.sqlite"
231 return Path(locate_profile(profile)) / "history.sqlite"
232
232
233 @catch_corrupt_db
233 @catch_corrupt_db
234 def init_db(self):
234 def init_db(self):
235 """Connect to the database, and create tables if necessary."""
235 """Connect to the database, and create tables if necessary."""
236 if not self.enabled:
236 if not self.enabled:
237 self.db = DummyDB()
237 self.db = DummyDB()
238 return
238 return
239
239
240 # use detect_types so that timestamps return datetime objects
240 # use detect_types so that timestamps return datetime objects
241 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
241 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES)
242 kwargs.update(self.connection_options)
242 kwargs.update(self.connection_options)
243 self.db = sqlite3.connect(str(self.hist_file), **kwargs)
243 self.db = sqlite3.connect(str(self.hist_file), **kwargs)
244 with self.db:
244 with self.db:
245 self.db.execute(
245 self.db.execute(
246 """CREATE TABLE IF NOT EXISTS sessions (session integer
246 """CREATE TABLE IF NOT EXISTS sessions (session integer
247 primary key autoincrement, start timestamp,
247 primary key autoincrement, start timestamp,
248 end timestamp, num_cmds integer, remark text)"""
248 end timestamp, num_cmds integer, remark text)"""
249 )
249 )
250 self.db.execute(
250 self.db.execute(
251 """CREATE TABLE IF NOT EXISTS history
251 """CREATE TABLE IF NOT EXISTS history
252 (session integer, line integer, source text, source_raw text,
252 (session integer, line integer, source text, source_raw text,
253 PRIMARY KEY (session, line))"""
253 PRIMARY KEY (session, line))"""
254 )
254 )
255 # Output history is optional, but ensure the table's there so it can be
255 # Output history is optional, but ensure the table's there so it can be
256 # enabled later.
256 # enabled later.
257 self.db.execute(
257 self.db.execute(
258 """CREATE TABLE IF NOT EXISTS output_history
258 """CREATE TABLE IF NOT EXISTS output_history
259 (session integer, line integer, output text,
259 (session integer, line integer, output text,
260 PRIMARY KEY (session, line))"""
260 PRIMARY KEY (session, line))"""
261 )
261 )
262 # success! reset corrupt db count
262 # success! reset corrupt db count
263 self._corrupt_db_counter = 0
263 self._corrupt_db_counter = 0
264
264
265 def writeout_cache(self):
265 def writeout_cache(self):
266 """Overridden by HistoryManager to dump the cache before certain
266 """Overridden by HistoryManager to dump the cache before certain
267 database lookups."""
267 database lookups."""
268 pass
268 pass
269
269
270 ## -------------------------------
270 ## -------------------------------
271 ## Methods for retrieving history:
271 ## Methods for retrieving history:
272 ## -------------------------------
272 ## -------------------------------
273 def _run_sql(self, sql, params, raw=True, output=False, latest=False):
273 def _run_sql(self, sql, params, raw=True, output=False, latest=False):
274 """Prepares and runs an SQL query for the history database.
274 """Prepares and runs an SQL query for the history database.
275
275
276 Parameters
276 Parameters
277 ----------
277 ----------
278 sql : str
278 sql : str
279 Any filtering expressions to go after SELECT ... FROM ...
279 Any filtering expressions to go after SELECT ... FROM ...
280 params : tuple
280 params : tuple
281 Parameters passed to the SQL query (to replace "?")
281 Parameters passed to the SQL query (to replace "?")
282 raw, output : bool
282 raw, output : bool
283 See :meth:`get_range`
283 See :meth:`get_range`
284 latest : bool
284 latest : bool
285 Select rows with max (session, line)
285 Select rows with max (session, line)
286
286
287 Returns
287 Returns
288 -------
288 -------
289 Tuples as :meth:`get_range`
289 Tuples as :meth:`get_range`
290 """
290 """
291 toget = 'source_raw' if raw else 'source'
291 toget = 'source_raw' if raw else 'source'
292 sqlfrom = "history"
292 sqlfrom = "history"
293 if output:
293 if output:
294 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
294 sqlfrom = "history LEFT JOIN output_history USING (session, line)"
295 toget = "history.%s, output_history.output" % toget
295 toget = "history.%s, output_history.output" % toget
296 if latest:
296 if latest:
297 toget += ", MAX(session * 128 * 1024 + line)"
297 toget += ", MAX(session * 128 * 1024 + line)"
298 this_querry = "SELECT session, line, %s FROM %s " % (toget, sqlfrom) + sql
298 this_querry = "SELECT session, line, %s FROM %s " % (toget, sqlfrom) + sql
299 cur = self.db.execute(this_querry, params)
299 cur = self.db.execute(this_querry, params)
300 if latest:
300 if latest:
301 cur = (row[:-1] for row in cur)
301 cur = (row[:-1] for row in cur)
302 if output: # Regroup into 3-tuples, and parse JSON
302 if output: # Regroup into 3-tuples, and parse JSON
303 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
303 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur)
304 return cur
304 return cur
305
305
306 @only_when_enabled
306 @only_when_enabled
307 @catch_corrupt_db
307 @catch_corrupt_db
308 def get_session_info(self, session):
308 def get_session_info(self, session):
309 """Get info about a session.
309 """Get info about a session.
310
310
311 Parameters
311 Parameters
312 ----------
312 ----------
313 session : int
313 session : int
314 Session number to retrieve.
314 Session number to retrieve.
315
315
316 Returns
316 Returns
317 -------
317 -------
318 session_id : int
318 session_id : int
319 Session ID number
319 Session ID number
320 start : datetime
320 start : datetime
321 Timestamp for the start of the session.
321 Timestamp for the start of the session.
322 end : datetime
322 end : datetime
323 Timestamp for the end of the session, or None if IPython crashed.
323 Timestamp for the end of the session, or None if IPython crashed.
324 num_cmds : int
324 num_cmds : int
325 Number of commands run, or None if IPython crashed.
325 Number of commands run, or None if IPython crashed.
326 remark : unicode
326 remark : unicode
327 A manually set description.
327 A manually set description.
328 """
328 """
329 query = "SELECT * from sessions where session == ?"
329 query = "SELECT * from sessions where session == ?"
330 return self.db.execute(query, (session,)).fetchone()
330 return self.db.execute(query, (session,)).fetchone()
331
331
332 @catch_corrupt_db
332 @catch_corrupt_db
333 def get_last_session_id(self):
333 def get_last_session_id(self):
334 """Get the last session ID currently in the database.
334 """Get the last session ID currently in the database.
335
335
336 Within IPython, this should be the same as the value stored in
336 Within IPython, this should be the same as the value stored in
337 :attr:`HistoryManager.session_number`.
337 :attr:`HistoryManager.session_number`.
338 """
338 """
339 for record in self.get_tail(n=1, include_latest=True):
339 for record in self.get_tail(n=1, include_latest=True):
340 return record[0]
340 return record[0]
341
341
342 @catch_corrupt_db
342 @catch_corrupt_db
343 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
343 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
344 """Get the last n lines from the history database.
344 """Get the last n lines from the history database.
345
345
346 Parameters
346 Parameters
347 ----------
347 ----------
348 n : int
348 n : int
349 The number of lines to get
349 The number of lines to get
350 raw, output : bool
350 raw, output : bool
351 See :meth:`get_range`
351 See :meth:`get_range`
352 include_latest : bool
352 include_latest : bool
353 If False (default), n+1 lines are fetched, and the latest one
353 If False (default), n+1 lines are fetched, and the latest one
354 is discarded. This is intended to be used where the function
354 is discarded. This is intended to be used where the function
355 is called by a user command, which it should not return.
355 is called by a user command, which it should not return.
356
356
357 Returns
357 Returns
358 -------
358 -------
359 Tuples as :meth:`get_range`
359 Tuples as :meth:`get_range`
360 """
360 """
361 self.writeout_cache()
361 self.writeout_cache()
362 if not include_latest:
362 if not include_latest:
363 n += 1
363 n += 1
364 cur = self._run_sql("ORDER BY session DESC, line DESC LIMIT ?",
364 cur = self._run_sql(
365 (n,), raw=raw, output=output)
365 "ORDER BY session DESC, line DESC LIMIT ?", (n,), raw=raw, output=output
366 )
366 if not include_latest:
367 if not include_latest:
367 return reversed(list(cur)[1:])
368 return reversed(list(cur)[1:])
368 return reversed(list(cur))
369 return reversed(list(cur))
369
370
370 @catch_corrupt_db
371 @catch_corrupt_db
371 def search(self, pattern="*", raw=True, search_raw=True,
372 def search(self, pattern="*", raw=True, search_raw=True,
372 output=False, n=None, unique=False):
373 output=False, n=None, unique=False):
373 """Search the database using unix glob-style matching (wildcards
374 """Search the database using unix glob-style matching (wildcards
374 * and ?).
375 * and ?).
375
376
376 Parameters
377 Parameters
377 ----------
378 ----------
378 pattern : str
379 pattern : str
379 The wildcarded pattern to match when searching
380 The wildcarded pattern to match when searching
380 search_raw : bool
381 search_raw : bool
381 If True, search the raw input, otherwise, the parsed input
382 If True, search the raw input, otherwise, the parsed input
382 raw, output : bool
383 raw, output : bool
383 See :meth:`get_range`
384 See :meth:`get_range`
384 n : None or int
385 n : None or int
385 If an integer is given, it defines the limit of
386 If an integer is given, it defines the limit of
386 returned entries.
387 returned entries.
387 unique : bool
388 unique : bool
388 When it is true, return only unique entries.
389 When it is true, return only unique entries.
389
390
390 Returns
391 Returns
391 -------
392 -------
392 Tuples as :meth:`get_range`
393 Tuples as :meth:`get_range`
393 """
394 """
394 tosearch = "source_raw" if search_raw else "source"
395 tosearch = "source_raw" if search_raw else "source"
395 if output:
396 if output:
396 tosearch = "history." + tosearch
397 tosearch = "history." + tosearch
397 self.writeout_cache()
398 self.writeout_cache()
398 sqlform = "WHERE %s GLOB ?" % tosearch
399 sqlform = "WHERE %s GLOB ?" % tosearch
399 params = (pattern,)
400 params = (pattern,)
400 if unique:
401 if unique:
401 sqlform += ' GROUP BY {0}'.format(tosearch)
402 sqlform += ' GROUP BY {0}'.format(tosearch)
402 if n is not None:
403 if n is not None:
403 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
404 sqlform += " ORDER BY session DESC, line DESC LIMIT ?"
404 params += (n,)
405 params += (n,)
405 elif unique:
406 elif unique:
406 sqlform += " ORDER BY session, line"
407 sqlform += " ORDER BY session, line"
407 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique)
408 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique)
408 if n is not None:
409 if n is not None:
409 return reversed(list(cur))
410 return reversed(list(cur))
410 return cur
411 return cur
411
412
412 @catch_corrupt_db
413 @catch_corrupt_db
413 def get_range(self, session, start=1, stop=None, raw=True,output=False):
414 def get_range(self, session, start=1, stop=None, raw=True,output=False):
414 """Retrieve input by session.
415 """Retrieve input by session.
415
416
416 Parameters
417 Parameters
417 ----------
418 ----------
418 session : int
419 session : int
419 Session number to retrieve.
420 Session number to retrieve.
420 start : int
421 start : int
421 First line to retrieve.
422 First line to retrieve.
422 stop : int
423 stop : int
423 End of line range (excluded from output itself). If None, retrieve
424 End of line range (excluded from output itself). If None, retrieve
424 to the end of the session.
425 to the end of the session.
425 raw : bool
426 raw : bool
426 If True, return untranslated input
427 If True, return untranslated input
427 output : bool
428 output : bool
428 If True, attempt to include output. This will be 'real' Python
429 If True, attempt to include output. This will be 'real' Python
429 objects for the current session, or text reprs from previous
430 objects for the current session, or text reprs from previous
430 sessions if db_log_output was enabled at the time. Where no output
431 sessions if db_log_output was enabled at the time. Where no output
431 is found, None is used.
432 is found, None is used.
432
433
433 Returns
434 Returns
434 -------
435 -------
435 entries
436 entries
436 An iterator over the desired lines. Each line is a 3-tuple, either
437 An iterator over the desired lines. Each line is a 3-tuple, either
437 (session, line, input) if output is False, or
438 (session, line, input) if output is False, or
438 (session, line, (input, output)) if output is True.
439 (session, line, (input, output)) if output is True.
439 """
440 """
440 if stop:
441 if stop:
441 lineclause = "line >= ? AND line < ?"
442 lineclause = "line >= ? AND line < ?"
442 params = (session, start, stop)
443 params = (session, start, stop)
443 else:
444 else:
444 lineclause = "line>=?"
445 lineclause = "line>=?"
445 params = (session, start)
446 params = (session, start)
446
447
447 return self._run_sql("WHERE session==? AND %s" % lineclause,
448 return self._run_sql("WHERE session==? AND %s" % lineclause,
448 params, raw=raw, output=output)
449 params, raw=raw, output=output)
449
450
450 def get_range_by_str(self, rangestr, raw=True, output=False):
451 def get_range_by_str(self, rangestr, raw=True, output=False):
451 """Get lines of history from a string of ranges, as used by magic
452 """Get lines of history from a string of ranges, as used by magic
452 commands %hist, %save, %macro, etc.
453 commands %hist, %save, %macro, etc.
453
454
454 Parameters
455 Parameters
455 ----------
456 ----------
456 rangestr : str
457 rangestr : str
457 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used,
458 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used,
458 this will return everything from current session's history.
459 this will return everything from current session's history.
459
460
460 See the documentation of :func:`%history` for the full details.
461 See the documentation of :func:`%history` for the full details.
461
462
462 raw, output : bool
463 raw, output : bool
463 As :meth:`get_range`
464 As :meth:`get_range`
464
465
465 Returns
466 Returns
466 -------
467 -------
467 Tuples as :meth:`get_range`
468 Tuples as :meth:`get_range`
468 """
469 """
469 for sess, s, e in extract_hist_ranges(rangestr):
470 for sess, s, e in extract_hist_ranges(rangestr):
470 for line in self.get_range(sess, s, e, raw=raw, output=output):
471 for line in self.get_range(sess, s, e, raw=raw, output=output):
471 yield line
472 yield line
472
473
473
474
474 class HistoryManager(HistoryAccessor):
475 class HistoryManager(HistoryAccessor):
475 """A class to organize all history-related functionality in one place.
476 """A class to organize all history-related functionality in one place.
476 """
477 """
477 # Public interface
478 # Public interface
478
479
479 # An instance of the IPython shell we are attached to
480 # An instance of the IPython shell we are attached to
480 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
481 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC',
481 allow_none=True)
482 allow_none=True)
482 # Lists to hold processed and raw history. These start with a blank entry
483 # Lists to hold processed and raw history. These start with a blank entry
483 # so that we can index them starting from 1
484 # so that we can index them starting from 1
484 input_hist_parsed = List([""])
485 input_hist_parsed = List([""])
485 input_hist_raw = List([""])
486 input_hist_raw = List([""])
486 # A list of directories visited during session
487 # A list of directories visited during session
487 dir_hist = List()
488 dir_hist = List()
488 @default('dir_hist')
489 @default('dir_hist')
489 def _dir_hist_default(self):
490 def _dir_hist_default(self):
490 try:
491 try:
491 return [Path.cwd()]
492 return [Path.cwd()]
492 except OSError:
493 except OSError:
493 return []
494 return []
494
495
495 # A dict of output history, keyed with ints from the shell's
496 # A dict of output history, keyed with ints from the shell's
496 # execution count.
497 # execution count.
497 output_hist = Dict()
498 output_hist = Dict()
498 # The text/plain repr of outputs.
499 # The text/plain repr of outputs.
499 output_hist_reprs = Dict()
500 output_hist_reprs = Dict()
500
501
501 # The number of the current session in the history database
502 # The number of the current session in the history database
502 session_number = Integer()
503 session_number = Integer()
503
504
504 db_log_output = Bool(False,
505 db_log_output = Bool(False,
505 help="Should the history database include output? (default: no)"
506 help="Should the history database include output? (default: no)"
506 ).tag(config=True)
507 ).tag(config=True)
507 db_cache_size = Integer(0,
508 db_cache_size = Integer(0,
508 help="Write to database every x commands (higher values save disk access & power).\n"
509 help="Write to database every x commands (higher values save disk access & power).\n"
509 "Values of 1 or less effectively disable caching."
510 "Values of 1 or less effectively disable caching."
510 ).tag(config=True)
511 ).tag(config=True)
511 # The input and output caches
512 # The input and output caches
512 db_input_cache = List()
513 db_input_cache = List()
513 db_output_cache = List()
514 db_output_cache = List()
514
515
515 # History saving in separate thread
516 # History saving in separate thread
516 save_thread = Instance('IPython.core.history.HistorySavingThread',
517 save_thread = Instance('IPython.core.history.HistorySavingThread',
517 allow_none=True)
518 allow_none=True)
518 save_flag = Instance(threading.Event, allow_none=True)
519 save_flag = Instance(threading.Event, allow_none=True)
519
520
520 # Private interface
521 # Private interface
521 # Variables used to store the three last inputs from the user. On each new
522 # Variables used to store the three last inputs from the user. On each new
522 # history update, we populate the user's namespace with these, shifted as
523 # history update, we populate the user's namespace with these, shifted as
523 # necessary.
524 # necessary.
524 _i00 = Unicode(u'')
525 _i00 = Unicode(u'')
525 _i = Unicode(u'')
526 _i = Unicode(u'')
526 _ii = Unicode(u'')
527 _ii = Unicode(u'')
527 _iii = Unicode(u'')
528 _iii = Unicode(u'')
528
529
529 # A regex matching all forms of the exit command, so that we don't store
530 # A regex matching all forms of the exit command, so that we don't store
530 # them in the history (it's annoying to rewind the first entry and land on
531 # them in the history (it's annoying to rewind the first entry and land on
531 # an exit call).
532 # an exit call).
532 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
533 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$")
533
534
534 def __init__(self, shell=None, config=None, **traits):
535 def __init__(self, shell=None, config=None, **traits):
535 """Create a new history manager associated with a shell instance.
536 """Create a new history manager associated with a shell instance.
536 """
537 """
537 super(HistoryManager, self).__init__(shell=shell, config=config,
538 super(HistoryManager, self).__init__(shell=shell, config=config,
538 **traits)
539 **traits)
539 self.save_flag = threading.Event()
540 self.save_flag = threading.Event()
540 self.db_input_cache_lock = threading.Lock()
541 self.db_input_cache_lock = threading.Lock()
541 self.db_output_cache_lock = threading.Lock()
542 self.db_output_cache_lock = threading.Lock()
542
543
543 try:
544 try:
544 self.new_session()
545 self.new_session()
545 except sqlite3.OperationalError:
546 except sqlite3.OperationalError:
546 self.log.error("Failed to create history session in %s. History will not be saved.",
547 self.log.error("Failed to create history session in %s. History will not be saved.",
547 self.hist_file, exc_info=True)
548 self.hist_file, exc_info=True)
548 self.hist_file = ':memory:'
549 self.hist_file = ':memory:'
549
550
550 if self.enabled and self.hist_file != ':memory:':
551 if self.enabled and self.hist_file != ':memory:':
551 self.save_thread = HistorySavingThread(self)
552 self.save_thread = HistorySavingThread(self)
552 self.save_thread.start()
553 self.save_thread.start()
553
554
554 def _get_hist_file_name(self, profile=None):
555 def _get_hist_file_name(self, profile=None):
555 """Get default history file name based on the Shell's profile.
556 """Get default history file name based on the Shell's profile.
556
557
557 The profile parameter is ignored, but must exist for compatibility with
558 The profile parameter is ignored, but must exist for compatibility with
558 the parent class."""
559 the parent class."""
559 profile_dir = self.shell.profile_dir.location
560 profile_dir = self.shell.profile_dir.location
560 return Path(profile_dir) / "history.sqlite"
561 return Path(profile_dir) / "history.sqlite"
561
562
562 @only_when_enabled
563 @only_when_enabled
563 def new_session(self, conn=None):
564 def new_session(self, conn=None):
564 """Get a new session number."""
565 """Get a new session number."""
565 if conn is None:
566 if conn is None:
566 conn = self.db
567 conn = self.db
567
568
568 with conn:
569 with conn:
569 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
570 cur = conn.execute("""INSERT INTO sessions VALUES (NULL, ?, NULL,
570 NULL, "") """, (datetime.datetime.now(),))
571 NULL, "") """, (datetime.datetime.now(),))
571 self.session_number = cur.lastrowid
572 self.session_number = cur.lastrowid
572
573
573 def end_session(self):
574 def end_session(self):
574 """Close the database session, filling in the end time and line count."""
575 """Close the database session, filling in the end time and line count."""
575 self.writeout_cache()
576 self.writeout_cache()
576 with self.db:
577 with self.db:
577 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
578 self.db.execute("""UPDATE sessions SET end=?, num_cmds=? WHERE
578 session==?""", (datetime.datetime.now(),
579 session==?""", (datetime.datetime.now(),
579 len(self.input_hist_parsed)-1, self.session_number))
580 len(self.input_hist_parsed)-1, self.session_number))
580 self.session_number = 0
581 self.session_number = 0
581
582
582 def name_session(self, name):
583 def name_session(self, name):
583 """Give the current session a name in the history database."""
584 """Give the current session a name in the history database."""
584 with self.db:
585 with self.db:
585 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
586 self.db.execute("UPDATE sessions SET remark=? WHERE session==?",
586 (name, self.session_number))
587 (name, self.session_number))
587
588
588 def reset(self, new_session=True):
589 def reset(self, new_session=True):
589 """Clear the session history, releasing all object references, and
590 """Clear the session history, releasing all object references, and
590 optionally open a new session."""
591 optionally open a new session."""
591 self.output_hist.clear()
592 self.output_hist.clear()
592 # The directory history can't be completely empty
593 # The directory history can't be completely empty
593 self.dir_hist[:] = [Path.cwd()]
594 self.dir_hist[:] = [Path.cwd()]
594
595
595 if new_session:
596 if new_session:
596 if self.session_number:
597 if self.session_number:
597 self.end_session()
598 self.end_session()
598 self.input_hist_parsed[:] = [""]
599 self.input_hist_parsed[:] = [""]
599 self.input_hist_raw[:] = [""]
600 self.input_hist_raw[:] = [""]
600 self.new_session()
601 self.new_session()
601
602
602 # ------------------------------
603 # ------------------------------
603 # Methods for retrieving history
604 # Methods for retrieving history
604 # ------------------------------
605 # ------------------------------
605 def get_session_info(self, session=0):
606 def get_session_info(self, session=0):
606 """Get info about a session.
607 """Get info about a session.
607
608
608 Parameters
609 Parameters
609 ----------
610 ----------
610 session : int
611 session : int
611 Session number to retrieve. The current session is 0, and negative
612 Session number to retrieve. The current session is 0, and negative
612 numbers count back from current session, so -1 is the previous session.
613 numbers count back from current session, so -1 is the previous session.
613
614
614 Returns
615 Returns
615 -------
616 -------
616 session_id : int
617 session_id : int
617 Session ID number
618 Session ID number
618 start : datetime
619 start : datetime
619 Timestamp for the start of the session.
620 Timestamp for the start of the session.
620 end : datetime
621 end : datetime
621 Timestamp for the end of the session, or None if IPython crashed.
622 Timestamp for the end of the session, or None if IPython crashed.
622 num_cmds : int
623 num_cmds : int
623 Number of commands run, or None if IPython crashed.
624 Number of commands run, or None if IPython crashed.
624 remark : unicode
625 remark : unicode
625 A manually set description.
626 A manually set description.
626 """
627 """
627 if session <= 0:
628 if session <= 0:
628 session += self.session_number
629 session += self.session_number
629
630
630 return super(HistoryManager, self).get_session_info(session=session)
631 return super(HistoryManager, self).get_session_info(session=session)
631
632
632 @catch_corrupt_db
633 @catch_corrupt_db
633 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
634 def get_tail(self, n=10, raw=True, output=False, include_latest=False):
634 """Get the last n lines from the history database.
635 """Get the last n lines from the history database.
635
636
636 Most recent entry last.
637 Most recent entry last.
637
638
638 Completion will be reordered so that that the last ones are when
639 Completion will be reordered so that that the last ones are when
639 possible from current session.
640 possible from current session.
640
641
641 Parameters
642 Parameters
642 ----------
643 ----------
643 n : int
644 n : int
644 The number of lines to get
645 The number of lines to get
645 raw, output : bool
646 raw, output : bool
646 See :meth:`get_range`
647 See :meth:`get_range`
647 include_latest : bool
648 include_latest : bool
648 If False (default), n+1 lines are fetched, and the latest one
649 If False (default), n+1 lines are fetched, and the latest one
649 is discarded. This is intended to be used where the function
650 is discarded. This is intended to be used where the function
650 is called by a user command, which it should not return.
651 is called by a user command, which it should not return.
651
652
652 Returns
653 Returns
653 -------
654 -------
654 Tuples as :meth:`get_range`
655 Tuples as :meth:`get_range`
655 """
656 """
656 self.writeout_cache()
657 self.writeout_cache()
657 if not include_latest:
658 if not include_latest:
658 n += 1
659 n += 1
659 # cursor/line/entry
660 # cursor/line/entry
660 this_cur = list(
661 this_cur = list(
661 self._run_sql(
662 self._run_sql(
662 "WHERE session == ? ORDER BY line DESC LIMIT ? ",
663 "WHERE session == ? ORDER BY line DESC LIMIT ? ",
663 (self.session_number, n),
664 (self.session_number, n),
664 raw=raw,
665 raw=raw,
665 output=output,
666 output=output,
666 )
667 )
667 )
668 )
668 other_cur = list(
669 other_cur = list(
669 self._run_sql(
670 self._run_sql(
670 "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?",
671 "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?",
671 (self.session_number, n),
672 (self.session_number, n),
672 raw=raw,
673 raw=raw,
673 output=output,
674 output=output,
674 )
675 )
675 )
676 )
676
677
677 everything = this_cur + other_cur
678 everything = this_cur + other_cur
678
679
679 everything = everything[:n]
680 everything = everything[:n]
680
681
681 if not include_latest:
682 if not include_latest:
682 return list(everything)[:0:-1]
683 return list(everything)[:0:-1]
683 return list(everything)[::-1]
684 return list(everything)[::-1]
684
685
685 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
686 def _get_range_session(self, start=1, stop=None, raw=True, output=False):
686 """Get input and output history from the current session. Called by
687 """Get input and output history from the current session. Called by
687 get_range, and takes similar parameters."""
688 get_range, and takes similar parameters."""
688 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
689 input_hist = self.input_hist_raw if raw else self.input_hist_parsed
689
690
690 n = len(input_hist)
691 n = len(input_hist)
691 if start < 0:
692 if start < 0:
692 start += n
693 start += n
693 if not stop or (stop > n):
694 if not stop or (stop > n):
694 stop = n
695 stop = n
695 elif stop < 0:
696 elif stop < 0:
696 stop += n
697 stop += n
697
698
698 for i in range(start, stop):
699 for i in range(start, stop):
699 if output:
700 if output:
700 line = (input_hist[i], self.output_hist_reprs.get(i))
701 line = (input_hist[i], self.output_hist_reprs.get(i))
701 else:
702 else:
702 line = input_hist[i]
703 line = input_hist[i]
703 yield (0, i, line)
704 yield (0, i, line)
704
705
705 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
706 def get_range(self, session=0, start=1, stop=None, raw=True,output=False):
706 """Retrieve input by session.
707 """Retrieve input by session.
707
708
708 Parameters
709 Parameters
709 ----------
710 ----------
710 session : int
711 session : int
711 Session number to retrieve. The current session is 0, and negative
712 Session number to retrieve. The current session is 0, and negative
712 numbers count back from current session, so -1 is previous session.
713 numbers count back from current session, so -1 is previous session.
713 start : int
714 start : int
714 First line to retrieve.
715 First line to retrieve.
715 stop : int
716 stop : int
716 End of line range (excluded from output itself). If None, retrieve
717 End of line range (excluded from output itself). If None, retrieve
717 to the end of the session.
718 to the end of the session.
718 raw : bool
719 raw : bool
719 If True, return untranslated input
720 If True, return untranslated input
720 output : bool
721 output : bool
721 If True, attempt to include output. This will be 'real' Python
722 If True, attempt to include output. This will be 'real' Python
722 objects for the current session, or text reprs from previous
723 objects for the current session, or text reprs from previous
723 sessions if db_log_output was enabled at the time. Where no output
724 sessions if db_log_output was enabled at the time. Where no output
724 is found, None is used.
725 is found, None is used.
725
726
726 Returns
727 Returns
727 -------
728 -------
728 entries
729 entries
729 An iterator over the desired lines. Each line is a 3-tuple, either
730 An iterator over the desired lines. Each line is a 3-tuple, either
730 (session, line, input) if output is False, or
731 (session, line, input) if output is False, or
731 (session, line, (input, output)) if output is True.
732 (session, line, (input, output)) if output is True.
732 """
733 """
733 if session <= 0:
734 if session <= 0:
734 session += self.session_number
735 session += self.session_number
735 if session==self.session_number: # Current session
736 if session==self.session_number: # Current session
736 return self._get_range_session(start, stop, raw, output)
737 return self._get_range_session(start, stop, raw, output)
737 return super(HistoryManager, self).get_range(session, start, stop, raw,
738 return super(HistoryManager, self).get_range(session, start, stop, raw,
738 output)
739 output)
739
740
740 ## ----------------------------
741 ## ----------------------------
741 ## Methods for storing history:
742 ## Methods for storing history:
742 ## ----------------------------
743 ## ----------------------------
743 def store_inputs(self, line_num, source, source_raw=None):
744 def store_inputs(self, line_num, source, source_raw=None):
744 """Store source and raw input in history and create input cache
745 """Store source and raw input in history and create input cache
745 variables ``_i*``.
746 variables ``_i*``.
746
747
747 Parameters
748 Parameters
748 ----------
749 ----------
749 line_num : int
750 line_num : int
750 The prompt number of this input.
751 The prompt number of this input.
751 source : str
752 source : str
752 Python input.
753 Python input.
753 source_raw : str, optional
754 source_raw : str, optional
754 If given, this is the raw input without any IPython transformations
755 If given, this is the raw input without any IPython transformations
755 applied to it. If not given, ``source`` is used.
756 applied to it. If not given, ``source`` is used.
756 """
757 """
757 if source_raw is None:
758 if source_raw is None:
758 source_raw = source
759 source_raw = source
759 source = source.rstrip('\n')
760 source = source.rstrip('\n')
760 source_raw = source_raw.rstrip('\n')
761 source_raw = source_raw.rstrip('\n')
761
762
762 # do not store exit/quit commands
763 # do not store exit/quit commands
763 if self._exit_re.match(source_raw.strip()):
764 if self._exit_re.match(source_raw.strip()):
764 return
765 return
765
766
766 self.input_hist_parsed.append(source)
767 self.input_hist_parsed.append(source)
767 self.input_hist_raw.append(source_raw)
768 self.input_hist_raw.append(source_raw)
768
769
769 with self.db_input_cache_lock:
770 with self.db_input_cache_lock:
770 self.db_input_cache.append((line_num, source, source_raw))
771 self.db_input_cache.append((line_num, source, source_raw))
771 # Trigger to flush cache and write to DB.
772 # Trigger to flush cache and write to DB.
772 if len(self.db_input_cache) >= self.db_cache_size:
773 if len(self.db_input_cache) >= self.db_cache_size:
773 self.save_flag.set()
774 self.save_flag.set()
774
775
775 # update the auto _i variables
776 # update the auto _i variables
776 self._iii = self._ii
777 self._iii = self._ii
777 self._ii = self._i
778 self._ii = self._i
778 self._i = self._i00
779 self._i = self._i00
779 self._i00 = source_raw
780 self._i00 = source_raw
780
781
781 # hackish access to user namespace to create _i1,_i2... dynamically
782 # hackish access to user namespace to create _i1,_i2... dynamically
782 new_i = '_i%s' % line_num
783 new_i = '_i%s' % line_num
783 to_main = {'_i': self._i,
784 to_main = {'_i': self._i,
784 '_ii': self._ii,
785 '_ii': self._ii,
785 '_iii': self._iii,
786 '_iii': self._iii,
786 new_i : self._i00 }
787 new_i : self._i00 }
787
788
788 if self.shell is not None:
789 if self.shell is not None:
789 self.shell.push(to_main, interactive=False)
790 self.shell.push(to_main, interactive=False)
790
791
791 def store_output(self, line_num):
792 def store_output(self, line_num):
792 """If database output logging is enabled, this saves all the
793 """If database output logging is enabled, this saves all the
793 outputs from the indicated prompt number to the database. It's
794 outputs from the indicated prompt number to the database. It's
794 called by run_cell after code has been executed.
795 called by run_cell after code has been executed.
795
796
796 Parameters
797 Parameters
797 ----------
798 ----------
798 line_num : int
799 line_num : int
799 The line number from which to save outputs
800 The line number from which to save outputs
800 """
801 """
801 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
802 if (not self.db_log_output) or (line_num not in self.output_hist_reprs):
802 return
803 return
803 output = self.output_hist_reprs[line_num]
804 output = self.output_hist_reprs[line_num]
804
805
805 with self.db_output_cache_lock:
806 with self.db_output_cache_lock:
806 self.db_output_cache.append((line_num, output))
807 self.db_output_cache.append((line_num, output))
807 if self.db_cache_size <= 1:
808 if self.db_cache_size <= 1:
808 self.save_flag.set()
809 self.save_flag.set()
809
810
810 def _writeout_input_cache(self, conn):
811 def _writeout_input_cache(self, conn):
811 with conn:
812 with conn:
812 for line in self.db_input_cache:
813 for line in self.db_input_cache:
813 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
814 conn.execute("INSERT INTO history VALUES (?, ?, ?, ?)",
814 (self.session_number,)+line)
815 (self.session_number,)+line)
815
816
816 def _writeout_output_cache(self, conn):
817 def _writeout_output_cache(self, conn):
817 with conn:
818 with conn:
818 for line in self.db_output_cache:
819 for line in self.db_output_cache:
819 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
820 conn.execute("INSERT INTO output_history VALUES (?, ?, ?)",
820 (self.session_number,)+line)
821 (self.session_number,)+line)
821
822
822 @only_when_enabled
823 @only_when_enabled
823 def writeout_cache(self, conn=None):
824 def writeout_cache(self, conn=None):
824 """Write any entries in the cache to the database."""
825 """Write any entries in the cache to the database."""
825 if conn is None:
826 if conn is None:
826 conn = self.db
827 conn = self.db
827
828
828 with self.db_input_cache_lock:
829 with self.db_input_cache_lock:
829 try:
830 try:
830 self._writeout_input_cache(conn)
831 self._writeout_input_cache(conn)
831 except sqlite3.IntegrityError:
832 except sqlite3.IntegrityError:
832 self.new_session(conn)
833 self.new_session(conn)
833 print("ERROR! Session/line number was not unique in",
834 print("ERROR! Session/line number was not unique in",
834 "database. History logging moved to new session",
835 "database. History logging moved to new session",
835 self.session_number)
836 self.session_number)
836 try:
837 try:
837 # Try writing to the new session. If this fails, don't
838 # Try writing to the new session. If this fails, don't
838 # recurse
839 # recurse
839 self._writeout_input_cache(conn)
840 self._writeout_input_cache(conn)
840 except sqlite3.IntegrityError:
841 except sqlite3.IntegrityError:
841 pass
842 pass
842 finally:
843 finally:
843 self.db_input_cache = []
844 self.db_input_cache = []
844
845
845 with self.db_output_cache_lock:
846 with self.db_output_cache_lock:
846 try:
847 try:
847 self._writeout_output_cache(conn)
848 self._writeout_output_cache(conn)
848 except sqlite3.IntegrityError:
849 except sqlite3.IntegrityError:
849 print("!! Session/line number for output was not unique",
850 print("!! Session/line number for output was not unique",
850 "in database. Output will not be stored.")
851 "in database. Output will not be stored.")
851 finally:
852 finally:
852 self.db_output_cache = []
853 self.db_output_cache = []
853
854
854
855
855 class HistorySavingThread(threading.Thread):
856 class HistorySavingThread(threading.Thread):
856 """This thread takes care of writing history to the database, so that
857 """This thread takes care of writing history to the database, so that
857 the UI isn't held up while that happens.
858 the UI isn't held up while that happens.
858
859
859 It waits for the HistoryManager's save_flag to be set, then writes out
860 It waits for the HistoryManager's save_flag to be set, then writes out
860 the history cache. The main thread is responsible for setting the flag when
861 the history cache. The main thread is responsible for setting the flag when
861 the cache size reaches a defined threshold."""
862 the cache size reaches a defined threshold."""
862 daemon = True
863 daemon = True
863 stop_now = False
864 stop_now = False
864 enabled = True
865 enabled = True
865 def __init__(self, history_manager):
866 def __init__(self, history_manager):
866 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
867 super(HistorySavingThread, self).__init__(name="IPythonHistorySavingThread")
867 self.history_manager = history_manager
868 self.history_manager = history_manager
868 self.enabled = history_manager.enabled
869 self.enabled = history_manager.enabled
869 atexit.register(self.stop)
870 atexit.register(self.stop)
870
871
871 @only_when_enabled
872 @only_when_enabled
872 def run(self):
873 def run(self):
873 # We need a separate db connection per thread:
874 # We need a separate db connection per thread:
874 try:
875 try:
875 self.db = sqlite3.connect(
876 self.db = sqlite3.connect(
876 str(self.history_manager.hist_file),
877 str(self.history_manager.hist_file),
877 **self.history_manager.connection_options,
878 **self.history_manager.connection_options,
878 )
879 )
879 while True:
880 while True:
880 self.history_manager.save_flag.wait()
881 self.history_manager.save_flag.wait()
881 if self.stop_now:
882 if self.stop_now:
882 self.db.close()
883 self.db.close()
883 return
884 return
884 self.history_manager.save_flag.clear()
885 self.history_manager.save_flag.clear()
885 self.history_manager.writeout_cache(self.db)
886 self.history_manager.writeout_cache(self.db)
886 except Exception as e:
887 except Exception as e:
887 print(("The history saving thread hit an unexpected error (%s)."
888 print(("The history saving thread hit an unexpected error (%s)."
888 "History will not be written to the database.") % repr(e))
889 "History will not be written to the database.") % repr(e))
889
890
890 def stop(self):
891 def stop(self):
891 """This can be called from the main thread to safely stop this thread.
892 """This can be called from the main thread to safely stop this thread.
892
893
893 Note that it does not attempt to write out remaining history before
894 Note that it does not attempt to write out remaining history before
894 exiting. That should be done by calling the HistoryManager's
895 exiting. That should be done by calling the HistoryManager's
895 end_session method."""
896 end_session method."""
896 self.stop_now = True
897 self.stop_now = True
897 self.history_manager.save_flag.set()
898 self.history_manager.save_flag.set()
898 self.join()
899 self.join()
899
900
900
901
901 # To match, e.g. ~5/8-~2/3
902 # To match, e.g. ~5/8-~2/3
902 range_re = re.compile(r"""
903 range_re = re.compile(r"""
903 ((?P<startsess>~?\d+)/)?
904 ((?P<startsess>~?\d+)/)?
904 (?P<start>\d+)?
905 (?P<start>\d+)?
905 ((?P<sep>[\-:])
906 ((?P<sep>[\-:])
906 ((?P<endsess>~?\d+)/)?
907 ((?P<endsess>~?\d+)/)?
907 (?P<end>\d+))?
908 (?P<end>\d+))?
908 $""", re.VERBOSE)
909 $""", re.VERBOSE)
909
910
910
911
911 def extract_hist_ranges(ranges_str):
912 def extract_hist_ranges(ranges_str):
912 """Turn a string of history ranges into 3-tuples of (session, start, stop).
913 """Turn a string of history ranges into 3-tuples of (session, start, stop).
913
914
914 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
915 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current
915 session".
916 session".
916
917
917 Examples
918 Examples
918 --------
919 --------
919 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
920 >>> list(extract_hist_ranges("~8/5-~7/4 2"))
920 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
921 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)]
921 """
922 """
922 if ranges_str == "":
923 if ranges_str == "":
923 yield (0, 1, None) # Everything from current session
924 yield (0, 1, None) # Everything from current session
924 return
925 return
925
926
926 for range_str in ranges_str.split():
927 for range_str in ranges_str.split():
927 rmatch = range_re.match(range_str)
928 rmatch = range_re.match(range_str)
928 if not rmatch:
929 if not rmatch:
929 continue
930 continue
930 start = rmatch.group("start")
931 start = rmatch.group("start")
931 if start:
932 if start:
932 start = int(start)
933 start = int(start)
933 end = rmatch.group("end")
934 end = rmatch.group("end")
934 # If no end specified, get (a, a + 1)
935 # If no end specified, get (a, a + 1)
935 end = int(end) if end else start + 1
936 end = int(end) if end else start + 1
936 else: # start not specified
937 else: # start not specified
937 if not rmatch.group('startsess'): # no startsess
938 if not rmatch.group('startsess'): # no startsess
938 continue
939 continue
939 start = 1
940 start = 1
940 end = None # provide the entire session hist
941 end = None # provide the entire session hist
941
942
942 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
943 if rmatch.group("sep") == "-": # 1-3 == 1:4 --> [1, 2, 3]
943 end += 1
944 end += 1
944 startsess = rmatch.group("startsess") or "0"
945 startsess = rmatch.group("startsess") or "0"
945 endsess = rmatch.group("endsess") or startsess
946 endsess = rmatch.group("endsess") or startsess
946 startsess = int(startsess.replace("~","-"))
947 startsess = int(startsess.replace("~","-"))
947 endsess = int(endsess.replace("~","-"))
948 endsess = int(endsess.replace("~","-"))
948 assert endsess >= startsess, "start session must be earlier than end session"
949 assert endsess >= startsess, "start session must be earlier than end session"
949
950
950 if endsess == startsess:
951 if endsess == startsess:
951 yield (startsess, start, end)
952 yield (startsess, start, end)
952 continue
953 continue
953 # Multiple sessions in one range:
954 # Multiple sessions in one range:
954 yield (startsess, start, None)
955 yield (startsess, start, None)
955 for sess in range(startsess+1, endsess):
956 for sess in range(startsess+1, endsess):
956 yield (sess, 1, None)
957 yield (sess, 1, None)
957 yield (endsess, 1, end)
958 yield (endsess, 1, end)
958
959
959
960
960 def _format_lineno(session, line):
961 def _format_lineno(session, line):
961 """Helper function to format line numbers properly."""
962 """Helper function to format line numbers properly."""
962 if session == 0:
963 if session == 0:
963 return str(line)
964 return str(line)
964 return "%s#%s" % (session, line)
965 return "%s#%s" % (session, line)
@@ -1,294 +1,295 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the IPython tab-completion machinery.
2 """Tests for the IPython tab-completion machinery.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Module imports
5 # Module imports
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7
7
8 # stdlib
8 # stdlib
9 import io
9 import io
10 import sqlite3
10 import sqlite3
11 import sys
11 import sys
12 import tempfile
12 import tempfile
13 from datetime import datetime
13 from datetime import datetime
14 from pathlib import Path
14 from pathlib import Path
15
15
16 from tempfile import TemporaryDirectory
16 from tempfile import TemporaryDirectory
17 # our own packages
17 # our own packages
18 from traitlets.config.loader import Config
18 from traitlets.config.loader import Config
19
19
20 from IPython.core.history import HistoryAccessor, HistoryManager, extract_hist_ranges
20 from IPython.core.history import HistoryAccessor, HistoryManager, extract_hist_ranges
21
21
22
22
23 def test_proper_default_encoding():
23 def test_proper_default_encoding():
24 assert sys.getdefaultencoding() == "utf-8"
24 assert sys.getdefaultencoding() == "utf-8"
25
25
26 def test_history():
26 def test_history():
27 ip = get_ipython()
27 ip = get_ipython()
28 with TemporaryDirectory() as tmpdir:
28 with TemporaryDirectory() as tmpdir:
29 tmp_path = Path(tmpdir)
29 tmp_path = Path(tmpdir)
30 hist_manager_ori = ip.history_manager
30 hist_manager_ori = ip.history_manager
31 hist_file = tmp_path / "history.sqlite"
31 hist_file = tmp_path / "history.sqlite"
32 try:
32 try:
33 ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file)
33 ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file)
34 hist = ["a=1", "def f():\n test = 1\n return test", "b='β‚¬Γ†ΒΎΓ·ΓŸ'"]
34 hist = ["a=1", "def f():\n test = 1\n return test", "b='β‚¬Γ†ΒΎΓ·ΓŸ'"]
35 for i, h in enumerate(hist, start=1):
35 for i, h in enumerate(hist, start=1):
36 ip.history_manager.store_inputs(i, h)
36 ip.history_manager.store_inputs(i, h)
37
37
38 ip.history_manager.db_log_output = True
38 ip.history_manager.db_log_output = True
39 # Doesn't match the input, but we'll just check it's stored.
39 # Doesn't match the input, but we'll just check it's stored.
40 ip.history_manager.output_hist_reprs[3] = "spam"
40 ip.history_manager.output_hist_reprs[3] = "spam"
41 ip.history_manager.store_output(3)
41 ip.history_manager.store_output(3)
42
42
43 assert ip.history_manager.input_hist_raw == [""] + hist
43 assert ip.history_manager.input_hist_raw == [""] + hist
44
44
45 # Detailed tests for _get_range_session
45 # Detailed tests for _get_range_session
46 grs = ip.history_manager._get_range_session
46 grs = ip.history_manager._get_range_session
47 assert list(grs(start=2, stop=-1)) == list(zip([0], [2], hist[1:-1]))
47 assert list(grs(start=2, stop=-1)) == list(zip([0], [2], hist[1:-1]))
48 assert list(grs(start=-2)) == list(zip([0, 0], [2, 3], hist[-2:]))
48 assert list(grs(start=-2)) == list(zip([0, 0], [2, 3], hist[-2:]))
49 assert list(grs(output=True)) == list(
49 assert list(grs(output=True)) == list(
50 zip([0, 0, 0], [1, 2, 3], zip(hist, [None, None, "spam"]))
50 zip([0, 0, 0], [1, 2, 3], zip(hist, [None, None, "spam"]))
51 )
51 )
52
52
53 # Check whether specifying a range beyond the end of the current
53 # Check whether specifying a range beyond the end of the current
54 # session results in an error (gh-804)
54 # session results in an error (gh-804)
55 ip.run_line_magic("hist", "2-500")
55 ip.run_line_magic("hist", "2-500")
56
56
57 # Check that we can write non-ascii characters to a file
57 # Check that we can write non-ascii characters to a file
58 ip.run_line_magic("hist", "-f %s" % (tmp_path / "test1"))
58 ip.run_line_magic("hist", "-f %s" % (tmp_path / "test1"))
59 ip.run_line_magic("hist", "-pf %s" % (tmp_path / "test2"))
59 ip.run_line_magic("hist", "-pf %s" % (tmp_path / "test2"))
60 ip.run_line_magic("hist", "-nf %s" % (tmp_path / "test3"))
60 ip.run_line_magic("hist", "-nf %s" % (tmp_path / "test3"))
61 ip.run_line_magic("save", "%s 1-10" % (tmp_path / "test4"))
61 ip.run_line_magic("save", "%s 1-10" % (tmp_path / "test4"))
62
62
63 # New session
63 # New session
64 ip.history_manager.reset()
64 ip.history_manager.reset()
65 newcmds = ["z=5", "class X(object):\n pass", "k='p'", "z=5"]
65 newcmds = ["z=5", "class X(object):\n pass", "k='p'", "z=5"]
66 for i, cmd in enumerate(newcmds, start=1):
66 for i, cmd in enumerate(newcmds, start=1):
67 ip.history_manager.store_inputs(i, cmd)
67 ip.history_manager.store_inputs(i, cmd)
68 gothist = ip.history_manager.get_range(start=1, stop=4)
68 gothist = ip.history_manager.get_range(start=1, stop=4)
69 assert list(gothist) == list(zip([0, 0, 0], [1, 2, 3], newcmds))
69 assert list(gothist) == list(zip([0, 0, 0], [1, 2, 3], newcmds))
70 # Previous session:
70 # Previous session:
71 gothist = ip.history_manager.get_range(-1, 1, 4)
71 gothist = ip.history_manager.get_range(-1, 1, 4)
72 assert list(gothist) == list(zip([1, 1, 1], [1, 2, 3], hist))
72 assert list(gothist) == list(zip([1, 1, 1], [1, 2, 3], hist))
73
73
74 newhist = [(2, i, c) for (i, c) in enumerate(newcmds, 1)]
74 newhist = [(2, i, c) for (i, c) in enumerate(newcmds, 1)]
75
75
76 # Check get_hist_tail
76 # Check get_hist_tail
77 gothist = ip.history_manager.get_tail(5, output=True,
77 gothist = ip.history_manager.get_tail(5, output=True,
78 include_latest=True)
78 include_latest=True)
79 expected = [(1, 3, (hist[-1], "spam"))] \
79 expected = [(1, 3, (hist[-1], "spam"))] \
80 + [(s, n, (c, None)) for (s, n, c) in newhist]
80 + [(s, n, (c, None)) for (s, n, c) in newhist]
81 assert list(gothist) == expected
81 assert list(gothist) == expected
82
82
83 gothist = ip.history_manager.get_tail(2)
83 gothist = ip.history_manager.get_tail(2)
84 expected = newhist[-3:-1]
84 expected = newhist[-3:-1]
85 assert list(gothist) == expected
85 assert list(gothist) == expected
86
86
87 # Check get_hist_search
87 # Check get_hist_search
88
88
89 gothist = ip.history_manager.search("*test*")
89 gothist = ip.history_manager.search("*test*")
90 assert list(gothist) == [(1, 2, hist[1])]
90 assert list(gothist) == [(1, 2, hist[1])]
91
91
92 gothist = ip.history_manager.search("*=*")
92 gothist = ip.history_manager.search("*=*")
93 assert list(gothist) == [
93 assert list(gothist) == [
94 (1, 1, hist[0]),
94 (1, 1, hist[0]),
95 (1, 2, hist[1]),
95 (1, 2, hist[1]),
96 (1, 3, hist[2]),
96 (1, 3, hist[2]),
97 newhist[0],
97 newhist[0],
98 newhist[2],
98 newhist[2],
99 newhist[3],
99 newhist[3],
100 ]
100 ]
101
101
102 gothist = ip.history_manager.search("*=*", n=4)
102 gothist = ip.history_manager.search("*=*", n=4)
103 assert list(gothist) == [
103 assert list(gothist) == [
104 (1, 3, hist[2]),
104 (1, 3, hist[2]),
105 newhist[0],
105 newhist[0],
106 newhist[2],
106 newhist[2],
107 newhist[3],
107 newhist[3],
108 ]
108 ]
109
109
110 gothist = ip.history_manager.search("*=*", unique=True)
110 gothist = ip.history_manager.search("*=*", unique=True)
111 assert list(gothist) == [
111 assert list(gothist) == [
112 (1, 1, hist[0]),
112 (1, 1, hist[0]),
113 (1, 2, hist[1]),
113 (1, 2, hist[1]),
114 (1, 3, hist[2]),
114 (1, 3, hist[2]),
115 newhist[2],
115 newhist[2],
116 newhist[3],
116 newhist[3],
117 ]
117 ]
118
118
119 gothist = ip.history_manager.search("*=*", unique=True, n=3)
119 gothist = ip.history_manager.search("*=*", unique=True, n=3)
120 assert list(gothist) == [(1, 3, hist[2]), newhist[2], newhist[3]]
120 assert list(gothist) == [(1, 3, hist[2]), newhist[2], newhist[3]]
121
121
122 gothist = ip.history_manager.search("b*", output=True)
122 gothist = ip.history_manager.search("b*", output=True)
123 assert list(gothist) == [(1, 3, (hist[2], "spam"))]
123 assert list(gothist) == [(1, 3, (hist[2], "spam"))]
124
124
125 # Cross testing: check that magic %save can get previous session.
125 # Cross testing: check that magic %save can get previous session.
126 testfilename = (tmp_path / "test.py").resolve()
126 testfilename = (tmp_path / "test.py").resolve()
127 ip.run_line_magic("save", str(testfilename) + " ~1/1-3")
127 ip.run_line_magic("save", str(testfilename) + " ~1/1-3")
128 with io.open(testfilename, encoding="utf-8") as testfile:
128 with io.open(testfilename, encoding="utf-8") as testfile:
129 assert testfile.read() == "# coding: utf-8\n" + "\n".join(hist) + "\n"
129 assert testfile.read() == "# coding: utf-8\n" + "\n".join(hist) + "\n"
130
130
131 # Duplicate line numbers - check that it doesn't crash, and
131 # Duplicate line numbers - check that it doesn't crash, and
132 # gets a new session
132 # gets a new session
133 ip.history_manager.store_inputs(1, "rogue")
133 ip.history_manager.store_inputs(1, "rogue")
134 ip.history_manager.writeout_cache()
134 ip.history_manager.writeout_cache()
135 assert ip.history_manager.session_number == 3
135 assert ip.history_manager.session_number == 3
136
136
137 # Check that session and line values are not just max values
137 # Check that session and line values are not just max values
138 sessid, lineno, entry = newhist[-1]
138 sessid, lineno, entry = newhist[-1]
139 assert lineno > 1
139 assert lineno > 1
140 ip.history_manager.reset()
140 ip.history_manager.reset()
141 lineno = 1
141 lineno = 1
142 ip.history_manager.store_inputs(lineno, entry)
142 ip.history_manager.store_inputs(lineno, entry)
143 gothist = ip.history_manager.search("*=*", unique=True)
143 gothist = ip.history_manager.search("*=*", unique=True)
144 hist = list(gothist)[-1]
144 hist = list(gothist)[-1]
145 assert sessid < hist[0]
145 assert sessid < hist[0]
146 assert hist[1:] == (lineno, entry)
146 assert hist[1:] == (lineno, entry)
147 finally:
147 finally:
148 # Ensure saving thread is shut down before we try to clean up the files
148 # Ensure saving thread is shut down before we try to clean up the files
149 ip.history_manager.save_thread.stop()
149 ip.history_manager.save_thread.stop()
150 # Forcibly close database rather than relying on garbage collection
150 # Forcibly close database rather than relying on garbage collection
151 ip.history_manager.db.close()
151 ip.history_manager.db.close()
152 # Restore history manager
152 # Restore history manager
153 ip.history_manager = hist_manager_ori
153 ip.history_manager = hist_manager_ori
154
154
155
155
156 def test_extract_hist_ranges():
156 def test_extract_hist_ranges():
157 instr = "1 2/3 ~4/5-6 ~4/7-~4/9 ~9/2-~7/5 ~10/"
157 instr = "1 2/3 ~4/5-6 ~4/7-~4/9 ~9/2-~7/5 ~10/"
158 expected = [(0, 1, 2), # 0 == current session
158 expected = [(0, 1, 2), # 0 == current session
159 (2, 3, 4),
159 (2, 3, 4),
160 (-4, 5, 7),
160 (-4, 5, 7),
161 (-4, 7, 10),
161 (-4, 7, 10),
162 (-9, 2, None), # None == to end
162 (-9, 2, None), # None == to end
163 (-8, 1, None),
163 (-8, 1, None),
164 (-7, 1, 6),
164 (-7, 1, 6),
165 (-10, 1, None)]
165 (-10, 1, None)]
166 actual = list(extract_hist_ranges(instr))
166 actual = list(extract_hist_ranges(instr))
167 assert actual == expected
167 assert actual == expected
168
168
169
169
170 def test_extract_hist_ranges_empty_str():
170 def test_extract_hist_ranges_empty_str():
171 instr = ""
171 instr = ""
172 expected = [(0, 1, None)] # 0 == current session, None == to end
172 expected = [(0, 1, None)] # 0 == current session, None == to end
173 actual = list(extract_hist_ranges(instr))
173 actual = list(extract_hist_ranges(instr))
174 assert actual == expected
174 assert actual == expected
175
175
176
176
177 def test_magic_rerun():
177 def test_magic_rerun():
178 """Simple test for %rerun (no args -> rerun last line)"""
178 """Simple test for %rerun (no args -> rerun last line)"""
179 ip = get_ipython()
179 ip = get_ipython()
180 ip.run_cell("a = 10", store_history=True)
180 ip.run_cell("a = 10", store_history=True)
181 ip.run_cell("a += 1", store_history=True)
181 ip.run_cell("a += 1", store_history=True)
182 assert ip.user_ns["a"] == 11
182 assert ip.user_ns["a"] == 11
183 ip.run_cell("%rerun", store_history=True)
183 ip.run_cell("%rerun", store_history=True)
184 assert ip.user_ns["a"] == 12
184 assert ip.user_ns["a"] == 12
185
185
186 def test_timestamp_type():
186 def test_timestamp_type():
187 ip = get_ipython()
187 ip = get_ipython()
188 info = ip.history_manager.get_session_info()
188 info = ip.history_manager.get_session_info()
189 assert isinstance(info[1], datetime)
189 assert isinstance(info[1], datetime)
190
190
191 def test_hist_file_config():
191 def test_hist_file_config():
192 cfg = Config()
192 cfg = Config()
193 tfile = tempfile.NamedTemporaryFile(delete=False)
193 tfile = tempfile.NamedTemporaryFile(delete=False)
194 cfg.HistoryManager.hist_file = Path(tfile.name)
194 cfg.HistoryManager.hist_file = Path(tfile.name)
195 try:
195 try:
196 hm = HistoryManager(shell=get_ipython(), config=cfg)
196 hm = HistoryManager(shell=get_ipython(), config=cfg)
197 assert hm.hist_file == cfg.HistoryManager.hist_file
197 assert hm.hist_file == cfg.HistoryManager.hist_file
198 finally:
198 finally:
199 try:
199 try:
200 Path(tfile.name).unlink()
200 Path(tfile.name).unlink()
201 except OSError:
201 except OSError:
202 # same catch as in testing.tools.TempFileMixin
202 # same catch as in testing.tools.TempFileMixin
203 # On Windows, even though we close the file, we still can't
203 # On Windows, even though we close the file, we still can't
204 # delete it. I have no clue why
204 # delete it. I have no clue why
205 pass
205 pass
206
206
207 def test_histmanager_disabled():
207 def test_histmanager_disabled():
208 """Ensure that disabling the history manager doesn't create a database."""
208 """Ensure that disabling the history manager doesn't create a database."""
209 cfg = Config()
209 cfg = Config()
210 cfg.HistoryAccessor.enabled = False
210 cfg.HistoryAccessor.enabled = False
211
211
212 ip = get_ipython()
212 ip = get_ipython()
213 with TemporaryDirectory() as tmpdir:
213 with TemporaryDirectory() as tmpdir:
214 hist_manager_ori = ip.history_manager
214 hist_manager_ori = ip.history_manager
215 hist_file = Path(tmpdir) / "history.sqlite"
215 hist_file = Path(tmpdir) / "history.sqlite"
216 cfg.HistoryManager.hist_file = hist_file
216 cfg.HistoryManager.hist_file = hist_file
217 try:
217 try:
218 ip.history_manager = HistoryManager(shell=ip, config=cfg)
218 ip.history_manager = HistoryManager(shell=ip, config=cfg)
219 hist = ["a=1", "def f():\n test = 1\n return test", "b='β‚¬Γ†ΒΎΓ·ΓŸ'"]
219 hist = ["a=1", "def f():\n test = 1\n return test", "b='β‚¬Γ†ΒΎΓ·ΓŸ'"]
220 for i, h in enumerate(hist, start=1):
220 for i, h in enumerate(hist, start=1):
221 ip.history_manager.store_inputs(i, h)
221 ip.history_manager.store_inputs(i, h)
222 assert ip.history_manager.input_hist_raw == [""] + hist
222 assert ip.history_manager.input_hist_raw == [""] + hist
223 ip.history_manager.reset()
223 ip.history_manager.reset()
224 ip.history_manager.end_session()
224 ip.history_manager.end_session()
225 finally:
225 finally:
226 ip.history_manager = hist_manager_ori
226 ip.history_manager = hist_manager_ori
227
227
228 # hist_file should not be created
228 # hist_file should not be created
229 assert hist_file.exists() is False
229 assert hist_file.exists() is False
230
230
231
231 def test_get_tail_session_awareness():
232 def test_get_tail_session_awareness():
232 """Test .get_tail() is:
233 """Test .get_tail() is:
233 - session specific in HistoryManager
234 - session specific in HistoryManager
234 - session agnostic in HistoryAccessor
235 - session agnostic in HistoryAccessor
235 same for .get_last_session_id()
236 same for .get_last_session_id()
236 """
237 """
237 ip = get_ipython()
238 ip = get_ipython()
238 with TemporaryDirectory() as tmpdir:
239 with TemporaryDirectory() as tmpdir:
239 tmp_path = Path(tmpdir)
240 tmp_path = Path(tmpdir)
240 hist_file = tmp_path / "history.sqlite"
241 hist_file = tmp_path / "history.sqlite"
241 get_source = lambda x: x[2]
242 get_source = lambda x: x[2]
242
243
243 # hm1 creates a new session and adds history entries,
244 # hm1 creates a new session and adds history entries,
244 # ha catches up
245 # ha catches up
245 hm1 = HistoryManager(shell=ip, hist_file=hist_file)
246 hm1 = HistoryManager(shell=ip, hist_file=hist_file)
246 hm1_last_sid = hm1.get_last_session_id
247 hm1_last_sid = hm1.get_last_session_id
247 ha = HistoryAccessor(hist_file=hist_file)
248 ha = HistoryAccessor(hist_file=hist_file)
248 ha_last_sid = ha.get_last_session_id
249 ha_last_sid = ha.get_last_session_id
249
250
250 hist1 = ["a=1", "b=1", "c=1"]
251 hist1 = ["a=1", "b=1", "c=1"]
251 for i, h in enumerate(hist1 + [""], start=1):
252 for i, h in enumerate(hist1 + [""], start=1):
252 hm1.store_inputs(i, h)
253 hm1.store_inputs(i, h)
253 assert list(map(get_source, hm1.get_tail())) == hist1
254 assert list(map(get_source, hm1.get_tail())) == hist1
254 assert list(map(get_source, ha.get_tail())) == hist1
255 assert list(map(get_source, ha.get_tail())) == hist1
255 sid1 = hm1_last_sid()
256 sid1 = hm1_last_sid()
256 assert sid1 is not None
257 assert sid1 is not None
257 assert ha_last_sid() == sid1
258 assert ha_last_sid() == sid1
258
259
259 # hm2 creates a new session and adds entries,
260 # hm2 creates a new session and adds entries,
260 # ha catches up
261 # ha catches up
261 hm2 = HistoryManager(shell=ip, hist_file=hist_file)
262 hm2 = HistoryManager(shell=ip, hist_file=hist_file)
262 hm2_last_sid = hm2.get_last_session_id
263 hm2_last_sid = hm2.get_last_session_id
263
264
264 hist2 = ["a=2", "b=2", "c=2"]
265 hist2 = ["a=2", "b=2", "c=2"]
265 for i, h in enumerate(hist2 + [""], start=1):
266 for i, h in enumerate(hist2 + [""], start=1):
266 hm2.store_inputs(i, h)
267 hm2.store_inputs(i, h)
267 tail = hm2.get_tail(n=3)
268 tail = hm2.get_tail(n=3)
268 assert list(map(get_source, tail)) == hist2
269 assert list(map(get_source, tail)) == hist2
269 tail = ha.get_tail(n=3)
270 tail = ha.get_tail(n=3)
270 assert list(map(get_source, tail)) == hist2
271 assert list(map(get_source, tail)) == hist2
271 sid2 = hm2_last_sid()
272 sid2 = hm2_last_sid()
272 assert sid2 is not None
273 assert sid2 is not None
273 assert ha_last_sid() == sid2
274 assert ha_last_sid() == sid2
274 assert sid2 != sid1
275 assert sid2 != sid1
275
276
276 # but hm1 still maintains its point of reference
277 # but hm1 still maintains its point of reference
277 # and adding more entries to it doesn't change others
278 # and adding more entries to it doesn't change others
278 # immediate perspective
279 # immediate perspective
279 assert hm1_last_sid() == sid1
280 assert hm1_last_sid() == sid1
280 tail = hm1.get_tail(n=3)
281 tail = hm1.get_tail(n=3)
281 assert list(map(get_source, tail)) == hist1
282 assert list(map(get_source, tail)) == hist1
282
283
283 hist3 = ["a=3", "b=3", "c=3"]
284 hist3 = ["a=3", "b=3", "c=3"]
284 for i, h in enumerate(hist3 + [""], start=5):
285 for i, h in enumerate(hist3 + [""], start=5):
285 hm1.store_inputs(i, h)
286 hm1.store_inputs(i, h)
286 tail = hm1.get_tail(n=7)
287 tail = hm1.get_tail(n=7)
287 assert list(map(get_source, tail)) == hist1 + [""] + hist3
288 assert list(map(get_source, tail)) == hist1 + [""] + hist3
288 tail = hm2.get_tail(n=3)
289 tail = hm2.get_tail(n=3)
289 assert list(map(get_source, tail)) == hist2
290 assert list(map(get_source, tail)) == hist2
290 tail = ha.get_tail(n=3)
291 tail = ha.get_tail(n=3)
291 assert list(map(get_source, tail)) == hist2
292 assert list(map(get_source, tail)) == hist2
292 assert hm1_last_sid() == sid1
293 assert hm1_last_sid() == sid1
293 assert hm2_last_sid() == sid2
294 assert hm2_last_sid() == sid2
294 assert ha_last_sid() == sid2
295 assert ha_last_sid() == sid2
General Comments 0
You need to be logged in to leave comments. Login now