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