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