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