Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/IPython/core/history.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

549 statements  

1"""History related magics and functionality""" 

2 

3from __future__ import annotations 

4 

5# Copyright (c) IPython Development Team. 

6# Distributed under the terms of the Modified BSD License. 

7 

8 

9import atexit 

10import datetime 

11import os 

12import re 

13import weakref 

14 

15 

16import threading 

17from pathlib import Path 

18 

19import functools 

20from collections import defaultdict 

21from contextlib import contextmanager 

22from dataclasses import dataclass 

23from traitlets import ( 

24 Any, 

25 Bool, 

26 Dict, 

27 Instance, 

28 Integer, 

29 List, 

30 TraitError, 

31 Unicode, 

32 Union, 

33 default, 

34 observe, 

35) 

36from traitlets.config.configurable import LoggingConfigurable 

37 

38from IPython.paths import locate_profile 

39from IPython.utils.decorators import undoc 

40from typing import TYPE_CHECKING, ParamSpec 

41from collections.abc import Iterable 

42import typing 

43import typing as t 

44from typing import cast 

45from warnings import warn 

46from weakref import ref, WeakSet 

47 

48from collections.abc import Callable, Iterator 

49from weakref import ReferenceType 

50 

51 

52if TYPE_CHECKING: 

53 import sqlite3 

54 from types import TracebackType 

55 

56 from IPython.core.interactiveshell import InteractiveShell 

57 from traitlets.config import Config as Configuration 

58 

59# sqlite3 is optional: it is a pure-Python package wrapping the `_sqlite3` 

60# extension module, and CPython can be built (or packaged) without the latter. 

61# Importing it costs ~8 ms and 23 modules, and a session that never touches 

62# history never needs it, so everything below resolves it on first use. 

63# 

64# Note that `importlib.util.find_spec("sqlite3")` is *not* a valid 

65# availability check: the pure-Python package is on disk either way, and only 

66# the import of `_sqlite3` underneath it fails. Only trying the import tells 

67# the truth -- and it must catch `ImportError`, not just `ModuleNotFoundError`, 

68# since the extension can also be present but fail to load. 

69 

70 

71@functools.cache 

72def _sqlite3() -> t.Any: 

73 """Return the `sqlite3` module, with IPython's converter registered. 

74 

75 Raises `ImportError` if this Python has no working sqlite3. 

76 """ 

77 import sqlite3 

78 

79 sqlite3.register_converter( 

80 "timestamp", lambda val: datetime.datetime.fromisoformat(val.decode()) 

81 ) 

82 return sqlite3 

83 

84 

85@functools.cache 

86def _sqlite3_found() -> bool: 

87 """Whether this Python can actually import sqlite3.""" 

88 try: 

89 _sqlite3() 

90 except ImportError: 

91 return False 

92 return True 

93 

94 

95@functools.cache 

96def _db_errors() -> tuple[type[BaseException], ...]: 

97 """The sqlite3 errors to catch, or an empty tuple if it is unavailable. 

98 

99 An empty tuple in an `except` clause simply never matches, which is the 

100 right behaviour when there is no database to fail in the first place. 

101 """ 

102 if not _sqlite3_found(): 

103 return () 

104 sqlite3 = _sqlite3() 

105 return (sqlite3.DatabaseError, sqlite3.OperationalError) 

106 

107 

108@functools.cache 

109def _operational_error() -> tuple[type[BaseException], ...]: 

110 """`sqlite3.OperationalError`, or an empty tuple if unavailable.""" 

111 if not _sqlite3_found(): 

112 return () 

113 return (_sqlite3().OperationalError,) 

114 

115 

116InOrInOut = str | tuple[str, str | None] 

117 

118# ----------------------------------------------------------------------------- 

119# Classes and functions 

120# ----------------------------------------------------------------------------- 

121 

122 

123@undoc 

124class DummyDB: 

125 """Dummy DB that will act as a black hole for history. 

126 

127 Only used in the absence of sqlite""" 

128 

129 def execute(*args: typing.Any, **kwargs: typing.Any) -> list: 

130 return [] 

131 

132 def commit(self, *args: typing.Any, **kwargs: typing.Any) -> None: 

133 pass 

134 

135 def __enter__(self, *args: typing.Any, **kwargs: typing.Any) -> None: 

136 pass 

137 

138 def __exit__(self, *args: typing.Any, **kwargs: typing.Any) -> None: 

139 pass 

140 

141 def close(self, *args: typing.Any, **kwargs: typing.Any) -> None: 

142 pass 

143 

144 

145_P = ParamSpec("_P") 

146_R = t.TypeVar("_R") 

147 

148 

149def only_when_enabled(f: t.Callable[_P, _R]) -> t.Callable[_P, _R]: 

150 """Decorator: return an empty list in the absence of sqlite. 

151 

152 Typed as signature-preserving (like the ``decorator``-package version it 

153 replaces): the empty-list fallback for a disabled accessor is invisible to 

154 the type system, as before. 

155 """ 

156 

157 @functools.wraps(f) 

158 def wrapper(*a: _P.args, **kw: _P.kwargs) -> _R: 

159 self = cast("HistoryAccessor", a[0]) 

160 if not self.enabled: 

161 return cast(_R, []) 

162 else: 

163 return f(*a, **kw) 

164 

165 return wrapper 

166 

167 

168# use 16kB as threshold for whether a corrupt history db should be saved 

169# that should be at least 100 entries or so 

170_SAVE_DB_SIZE = 16384 

171 

172 

173def catch_corrupt_db(f: t.Callable[_P, _R]) -> t.Callable[_P, _R]: 

174 """A decorator which wraps HistoryAccessor method calls to catch errors from 

175 a corrupt SQLite database, move the old database out of the way, and create 

176 a new one. 

177 

178 We avoid clobbering larger databases because this may be triggered due to filesystem issues, 

179 not just a corrupt file. 

180 """ 

181 

182 @functools.wraps(f) 

183 def wrapper(*a: _P.args, **kw: _P.kwargs) -> _R: 

184 self = cast("HistoryAccessor", a[0]) 

185 try: 

186 return f(*a, **kw) 

187 except _db_errors() as e: 

188 self._corrupt_db_counter += 1 

189 self.log.error("Failed to open SQLite history %s (%s).", self.hist_file, e) 

190 if self.hist_file != ":memory:": 

191 if self._corrupt_db_counter > self._corrupt_db_limit: 

192 self.hist_file = ":memory:" 

193 self.log.error( 

194 "Failed to load history too many times, history will not be saved." 

195 ) 

196 elif self.hist_file.is_file(): 

197 # move the file out of the way 

198 base = str(self.hist_file.parent / self.hist_file.stem) 

199 ext = self.hist_file.suffix 

200 size = self.hist_file.stat().st_size 

201 if size >= _SAVE_DB_SIZE: 

202 # if there's significant content, avoid clobbering 

203 now = ( 

204 datetime.datetime.now(datetime.UTC) 

205 .isoformat() 

206 .replace(":", ".") 

207 ) 

208 newpath = base + "-corrupt-" + now + ext 

209 # don't clobber previous corrupt backups 

210 for i in range(100): 

211 if not Path(newpath).exists(): 

212 break 

213 else: 

214 newpath = base + "-corrupt-" + now + ("-%i" % i) + ext 

215 else: 

216 # not much content, possibly empty; don't worry about clobbering 

217 # maybe we should just delete it? 

218 newpath = base + "-corrupt" + ext 

219 self.hist_file.rename(newpath) 

220 self.log.error( 

221 "History file was moved to %s and a new file created.", newpath 

222 ) 

223 self.init_db() 

224 return cast(_R, []) 

225 else: 

226 # Failed with :memory:, something serious is wrong 

227 raise 

228 

229 return wrapper 

230 

231 

232class HistoryAccessorBase(LoggingConfigurable): 

233 """An abstract class for History Accessors""" 

234 

235 def get_tail( 

236 self, 

237 n: int | None = 10, 

238 raw: bool = True, 

239 output: bool = False, 

240 include_latest: bool = False, 

241 ) -> Iterable[tuple[int, int, InOrInOut]]: 

242 raise NotImplementedError 

243 

244 def search( 

245 self, 

246 pattern: str = "*", 

247 raw: bool = True, 

248 search_raw: bool = True, 

249 output: bool = False, 

250 n: int | None = None, 

251 unique: bool = False, 

252 ) -> Iterable[tuple[int, int, InOrInOut]]: 

253 raise NotImplementedError 

254 

255 def get_range( 

256 self, 

257 session: int, 

258 start: int = 1, 

259 stop: int | None = None, 

260 raw: bool = True, 

261 output: bool = False, 

262 ) -> Iterable[tuple[int, int, InOrInOut]]: 

263 raise NotImplementedError 

264 

265 def get_range_by_str( 

266 self, rangestr: str, raw: bool = True, output: bool = False 

267 ) -> Iterable[tuple[int, int, InOrInOut]]: 

268 raise NotImplementedError 

269 

270 

271class HistoryAccessor(HistoryAccessorBase): 

272 """Access the history database without adding to it. 

273 

274 This is intended for use by standalone history tools. IPython shells use 

275 HistoryManager, below, which is a subclass of this.""" 

276 

277 # counter for init_db retries, so we don't keep trying over and over 

278 _corrupt_db_counter = 0 

279 # after two failures, fallback on :memory: 

280 _corrupt_db_limit = 2 

281 

282 # String holding the path to the history file 

283 hist_file = Union( 

284 [Instance(Path), Unicode()], 

285 help="""Path to file to use for SQLite history database. 

286 

287 By default, IPython will put the history database in the IPython 

288 profile directory. If you would rather share one history among 

289 profiles, you can set this value in each, so that they are consistent. 

290 

291 Due to an issue with fcntl, SQLite is known to misbehave on some NFS 

292 mounts. If you see IPython hanging, try setting this to something on a 

293 local disk, e.g:: 

294 

295 ipython --HistoryManager.hist_file=/tmp/ipython_hist.sqlite 

296 

297 you can also use the specific value `:memory:` (including the colon 

298 at both end but not the back ticks), to avoid creating an history file. 

299 

300 """, 

301 ).tag(config=True) 

302 

303 enabled = Bool( 

304 help="""enable the SQLite history 

305 

306 set enabled=False to disable the SQLite history, 

307 in which case there will be no stored history, no SQLite connection, 

308 and no background saving thread. This may be necessary in some 

309 threaded environments where IPython is embedded. 

310 """, 

311 ).tag(config=True) 

312 

313 @default("enabled") 

314 def _enabled_default(self) -> bool: 

315 # dynamic rather than `Bool(_sqlite3_found())`: a static default is 

316 # evaluated when the class is created, which would import sqlite3 on 

317 # every `import IPython` 

318 return _sqlite3_found() 

319 

320 connection_options = Dict( 

321 help="""Options for configuring the SQLite connection 

322 

323 These options are passed as keyword args to sqlite3.connect 

324 when establishing database connections. 

325 """ 

326 ).tag(config=True) 

327 

328 @default("connection_options") 

329 def _default_connection_options(self) -> dict[str, bool]: 

330 return dict(check_same_thread=False) 

331 

332 # The SQLite database 

333 db = Any() 

334 

335 @observe("db") 

336 @only_when_enabled 

337 def _db_changed(self, change): # type: ignore [no-untyped-def] 

338 """validate the db, since it can be an Instance of two different types""" 

339 new = change["new"] 

340 connection_types = (DummyDB, _sqlite3().Connection) 

341 if not isinstance(new, connection_types): 

342 msg = "{}.db must be sqlite3 Connection or DummyDB, not {!r}".format( 

343 self.__class__.__name__, 

344 new, 

345 ) 

346 raise TraitError(msg) 

347 

348 def __init__( 

349 self, profile: str = "default", hist_file: str = "", **traits: typing.Any 

350 ) -> None: 

351 """Create a new history accessor. 

352 

353 Parameters 

354 ---------- 

355 profile : str 

356 The name of the profile from which to open history. 

357 hist_file : str 

358 Path to an SQLite history database stored by IPython. If specified, 

359 hist_file overrides profile. 

360 config : :class:`~traitlets.config.loader.Config` 

361 Config object. hist_file can also be set through this. 

362 """ 

363 super().__init__(**traits) 

364 # defer setting hist_file from kwarg until after init, 

365 # otherwise the default kwarg value would clobber any value 

366 # set by config 

367 if hist_file: 

368 self.hist_file = hist_file 

369 

370 try: 

371 self.hist_file 

372 except TraitError: 

373 # No one has set the hist_file, yet. 

374 self.hist_file = self._get_hist_file_name(profile) 

375 

376 self.init_db() 

377 

378 def _get_hist_file_name(self, profile: str = "default") -> Path: 

379 """Find the history file for the given profile name. 

380 

381 This is overridden by the HistoryManager subclass, to use the shell's 

382 active profile. 

383 

384 Parameters 

385 ---------- 

386 profile : str 

387 The name of a profile which has a history file. 

388 """ 

389 return Path(locate_profile(profile)) / "history.sqlite" 

390 

391 @catch_corrupt_db 

392 def init_db(self) -> None: 

393 """Connect to the database, and create tables if necessary.""" 

394 if not self.enabled: 

395 self.db = DummyDB() 

396 self._finalizer = weakref.finalize(self, lambda db: db.close(), self.db) 

397 return 

398 

399 # use detect_types so that timestamps return datetime objects 

400 sqlite3 = _sqlite3() 

401 kwargs = dict(detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) 

402 kwargs.update(self.connection_options) 

403 self.db = sqlite3.connect(str(self.hist_file), **kwargs) 

404 self._finalizer = weakref.finalize(self, lambda db: db.close(), self.db) 

405 with self.db: 

406 self.db.execute( 

407 """CREATE TABLE IF NOT EXISTS sessions (session integer 

408 primary key autoincrement, start timestamp, 

409 end timestamp, num_cmds integer, remark text)""" 

410 ) 

411 self.db.execute( 

412 """CREATE TABLE IF NOT EXISTS history 

413 (session integer, line integer, source text, source_raw text, 

414 PRIMARY KEY (session, line))""" 

415 ) 

416 # Output history is optional, but ensure the table's there so it can be 

417 # enabled later. 

418 self.db.execute( 

419 """CREATE TABLE IF NOT EXISTS output_history 

420 (session integer, line integer, output text, 

421 PRIMARY KEY (session, line))""" 

422 ) 

423 # success! reset corrupt db count 

424 self._corrupt_db_counter = 0 

425 

426 def close(self) -> None: 

427 """Close the SQLite database connection. 

428 

429 Prefer calling this to closing ``self.db`` directly: it gives 

430 subclasses (notably :class:`HistoryManager`) a single place to hook in 

431 the rest of their teardown. Safe to call more than once. 

432 """ 

433 self.db.close() 

434 

435 def __enter__(self) -> HistoryAccessor: 

436 """Support use as a context manager for deterministic cleanup:: 

437 

438 with HistoryAccessor(hist_file=path) as history: 

439 ... 

440 # connection is closed here 

441 

442 :class:`HistoryManager` additionally stops its saving thread on exit. 

443 """ 

444 return self 

445 

446 def __exit__( 

447 self, 

448 exc_type: type[BaseException] | None, 

449 exc_value: BaseException | None, 

450 traceback: TracebackType | None, 

451 ) -> None: 

452 self.close() 

453 

454 def writeout_cache(self) -> None: 

455 """Overridden by HistoryManager to dump the cache before certain 

456 database lookups.""" 

457 pass 

458 

459 ## ------------------------------- 

460 ## Methods for retrieving history: 

461 ## ------------------------------- 

462 def _run_sql( 

463 self, 

464 sql: str, 

465 params: tuple, 

466 raw: bool = True, 

467 output: bool = False, 

468 latest: bool = False, 

469 ) -> Iterable[tuple[int, int, InOrInOut]]: 

470 """Prepares and runs an SQL query for the history database. 

471 

472 Parameters 

473 ---------- 

474 sql : str 

475 Any filtering expressions to go after SELECT ... FROM ... 

476 params : tuple 

477 Parameters passed to the SQL query (to replace "?") 

478 raw, output : bool 

479 See :meth:`get_range` 

480 latest : bool 

481 Select rows with max (session, line) 

482 

483 Returns 

484 ------- 

485 Tuples as :meth:`get_range` 

486 """ 

487 toget = "source_raw" if raw else "source" 

488 sqlfrom = "history" 

489 if output: 

490 sqlfrom = "history LEFT JOIN output_history USING (session, line)" 

491 toget = "history.%s, output_history.output" % toget 

492 if latest: 

493 toget += ", MAX(session * 128 * 1024 + line)" 

494 this_querry = "SELECT session, line, {} FROM {} ".format(toget, sqlfrom) + sql 

495 cur = self.db.execute(this_querry, params) 

496 if latest: 

497 cur = (row[:-1] for row in cur) 

498 if output: # Regroup into 3-tuples, and parse JSON 

499 return ((ses, lin, (inp, out)) for ses, lin, inp, out in cur) 

500 return cur 

501 

502 @only_when_enabled 

503 @catch_corrupt_db 

504 def get_session_info( 

505 self, session: int 

506 ) -> tuple[int, datetime.datetime, datetime.datetime | None, int | None, str]: 

507 """Get info about a session. 

508 

509 Parameters 

510 ---------- 

511 session : int 

512 Session number to retrieve. 

513 

514 Returns 

515 ------- 

516 session_id : int 

517 Session ID number 

518 start : datetime 

519 Timestamp for the start of the session. 

520 end : datetime 

521 Timestamp for the end of the session, or None if IPython crashed. 

522 num_cmds : int 

523 Number of commands run, or None if IPython crashed. 

524 remark : str 

525 A manually set description. 

526 """ 

527 query = "SELECT * from sessions where session == ?" 

528 return self.db.execute(query, (session,)).fetchone() 

529 

530 @catch_corrupt_db 

531 def get_last_session_id(self) -> int | None: 

532 """Get the last session ID currently in the database. 

533 

534 Within IPython, this should be the same as the value stored in 

535 :attr:`HistoryManager.session_number`. 

536 """ 

537 for record in self.get_tail(n=1, include_latest=True): 

538 return record[0] 

539 return None 

540 

541 @catch_corrupt_db 

542 def get_tail( 

543 self, 

544 n: int | None = 10, 

545 raw: bool = True, 

546 output: bool = False, 

547 include_latest: bool = False, 

548 ) -> Iterable[tuple[int, int, InOrInOut]]: 

549 """Get the last n lines from the history database. 

550 

551 Parameters 

552 ---------- 

553 n : int or None 

554 The number of lines to get. None gets the default of 10, which a 

555 kernel ``history_request`` without ``n`` asks for. 

556 raw, output : bool 

557 See :meth:`get_range` 

558 include_latest : bool 

559 If False (default), n+1 lines are fetched, and the latest one 

560 is discarded. This is intended to be used where the function 

561 is called by a user command, which it should not return. 

562 

563 Returns 

564 ------- 

565 Tuples as :meth:`get_range` 

566 """ 

567 if n is None: 

568 n = 10 

569 self.writeout_cache() 

570 if not include_latest: 

571 n += 1 

572 cur = self._run_sql( 

573 "ORDER BY session DESC, line DESC LIMIT ?", (n,), raw=raw, output=output 

574 ) 

575 if not include_latest: 

576 return reversed(list(cur)[1:]) 

577 return reversed(list(cur)) 

578 

579 @catch_corrupt_db 

580 def search( 

581 self, 

582 pattern: str = "*", 

583 raw: bool = True, 

584 search_raw: bool = True, 

585 output: bool = False, 

586 n: int | None = None, 

587 unique: bool = False, 

588 ) -> Iterable[tuple[int, int, InOrInOut]]: 

589 """Search the database using unix glob-style matching (wildcards 

590 * and ?). 

591 

592 Parameters 

593 ---------- 

594 pattern : str 

595 The wildcarded pattern to match when searching 

596 search_raw : bool 

597 If True, search the raw input, otherwise, the parsed input 

598 raw, output : bool 

599 See :meth:`get_range` 

600 n : None or int 

601 If an integer is given, it defines the limit of 

602 returned entries. 

603 unique : bool 

604 When it is true, return only unique entries. 

605 

606 Returns 

607 ------- 

608 Tuples as :meth:`get_range` 

609 """ 

610 tosearch = "source_raw" if search_raw else "source" 

611 if output: 

612 tosearch = "history." + tosearch 

613 self.writeout_cache() 

614 sqlform = "WHERE %s GLOB ?" % tosearch 

615 params: tuple[typing.Any, ...] = (pattern,) 

616 if unique: 

617 sqlform += f" GROUP BY {tosearch}" 

618 if n is not None: 

619 sqlform += " ORDER BY session DESC, line DESC LIMIT ?" 

620 params += (n,) 

621 elif unique: 

622 sqlform += " ORDER BY session, line" 

623 cur = self._run_sql(sqlform, params, raw=raw, output=output, latest=unique) 

624 if n is not None: 

625 return reversed(list(cur)) 

626 return cur 

627 

628 @catch_corrupt_db 

629 def get_range( 

630 self, 

631 session: int, 

632 start: int = 1, 

633 stop: int | None = None, 

634 raw: bool = True, 

635 output: bool = False, 

636 ) -> Iterable[tuple[int, int, InOrInOut]]: 

637 """Retrieve input by session. 

638 

639 Parameters 

640 ---------- 

641 session : int 

642 Session number to retrieve. 

643 start : int 

644 First line to retrieve. 

645 stop : int 

646 End of line range (excluded from output itself). If None, retrieve 

647 to the end of the session. 

648 raw : bool 

649 If True, return untranslated input 

650 output : bool 

651 If True, attempt to include output. This will be 'real' Python 

652 objects for the current session, or text reprs from previous 

653 sessions if db_log_output was enabled at the time. Where no output 

654 is found, None is used. 

655 

656 Returns 

657 ------- 

658 entries 

659 An iterator over the desired lines. Each line is a 3-tuple, either 

660 (session, line, input) if output is False, or 

661 (session, line, (input, output)) if output is True. 

662 """ 

663 params: tuple[typing.Any, ...] 

664 if stop: 

665 lineclause = "line >= ? AND line < ?" 

666 params = (session, start, stop) 

667 else: 

668 lineclause = "line>=?" 

669 params = (session, start) 

670 

671 return self._run_sql( 

672 "WHERE session==? AND %s" % lineclause, params, raw=raw, output=output 

673 ) 

674 

675 def get_range_by_str( 

676 self, rangestr: str, raw: bool = True, output: bool = False 

677 ) -> Iterable[tuple[int, int, InOrInOut]]: 

678 """Get lines of history from a string of ranges, as used by magic 

679 commands %hist, %save, %macro, etc. 

680 

681 Parameters 

682 ---------- 

683 rangestr : str 

684 A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used, 

685 this will return everything from current session's history. 

686 

687 See the documentation of :func:`%history` for the full details. 

688 

689 raw, output : bool 

690 As :meth:`get_range` 

691 

692 Returns 

693 ------- 

694 Tuples as :meth:`get_range` 

695 """ 

696 for sess, s, e in extract_hist_ranges(rangestr): 

697 yield from self.get_range(sess, s, e, raw=raw, output=output) 

698 

699 

700@dataclass 

701class HistoryOutput: 

702 output_type: typing.Literal[ 

703 "out_stream", "err_stream", "display_data", "execute_result" 

704 ] 

705 bundle: dict[str, str | list[str]] 

706 

707 

708class HistoryManager(HistoryAccessor): 

709 """A class to organize all history-related functionality in one place.""" 

710 

711 # Public interface 

712 

713 # An instance of the IPython shell we are attached to 

714 shell = Instance( 

715 "IPython.core.interactiveshell.InteractiveShellABC", allow_none=False 

716 ) 

717 # Lists to hold processed and raw history. These start with a blank entry 

718 # so that we can index them starting from 1 

719 input_hist_parsed = List([""]) 

720 input_hist_raw = List([""]) 

721 # A list of directories visited during session 

722 dir_hist: List = List() 

723 

724 @default("dir_hist") 

725 def _dir_hist_default(self) -> list[Path]: 

726 try: 

727 return [Path.cwd()] 

728 except OSError: 

729 return [] 

730 

731 # A dict of output history, keyed with ints from the shell's 

732 # execution count. 

733 output_hist = Dict() 

734 # The text/plain repr of outputs. 

735 output_hist_reprs: dict[int, str] = Dict() # type: ignore [assignment] 

736 # Maps execution_count to MIME bundles 

737 outputs: dict[int, list[HistoryOutput]] = defaultdict(list) 

738 # Maps execution_count to exception tracebacks 

739 exceptions: dict[int, dict[str, Any]] = Dict() # type: ignore [assignment] 

740 

741 # The number of the current session in the history database 

742 session_number: int = Integer() # type: ignore [assignment] 

743 

744 db_log_output = Bool( 

745 False, help="Should the history database include output? (default: no)" 

746 ).tag(config=True) 

747 db_cache_size = Integer( 

748 0, 

749 help="Write to database every x commands (higher values save disk access & power).\n" 

750 "Values of 1 or less effectively disable caching.", 

751 ).tag(config=True) 

752 # The input and output caches 

753 db_input_cache: List[tuple[int, str, str]] = List() 

754 db_output_cache: List[tuple[int, str]] = List() 

755 

756 # History saving in separate thread 

757 save_thread = Instance("IPython.core.history.HistorySavingThread", allow_none=True) 

758 

759 @property 

760 def save_flag(self) -> threading.Event | None: 

761 if self.save_thread is not None: 

762 return self.save_thread.save_flag 

763 return None 

764 

765 # Private interface 

766 # Variables used to store the three last inputs from the user. On each new 

767 # history update, we populate the user's namespace with these, shifted as 

768 # necessary. 

769 _i00 = Unicode("") 

770 _i = Unicode("") 

771 _ii = Unicode("") 

772 _iii = Unicode("") 

773 

774 # A regex matching all forms of the exit command, so that we don't store 

775 # them in the history (it's annoying to rewind the first entry and land on 

776 # an exit call). 

777 _exit_re = re.compile(r"(exit|quit)(\s*\(.*\))?$") 

778 

779 _instances: WeakSet[HistoryManager] = WeakSet() 

780 _max_inst: int | float = float("inf") 

781 

782 def __init__( 

783 self, 

784 shell: InteractiveShell, 

785 config: Configuration | None = None, 

786 **traits: typing.Any, 

787 ): 

788 """Create a new history manager associated with a shell instance.""" 

789 super().__init__(shell=shell, config=config, **traits) 

790 self.db_input_cache_lock = threading.Lock() 

791 self.db_output_cache_lock = threading.Lock() 

792 

793 try: 

794 self.new_session() 

795 except _operational_error(): 

796 self.log.error( 

797 "Failed to create history session in %s. History will not be saved.", 

798 self.hist_file, 

799 exc_info=True, 

800 ) 

801 self._switch_to_memory_history() 

802 

803 self.using_thread = False 

804 if self.enabled and self.hist_file != ":memory:": 

805 self.save_thread = HistorySavingThread(self) 

806 try: 

807 self.save_thread.start() 

808 except RuntimeError: 

809 self.log.error( 

810 "Failed to start history saving thread. History will not be saved.", 

811 exc_info=True, 

812 ) 

813 self._switch_to_memory_history() 

814 self.save_thread = None 

815 else: 

816 self.using_thread = True 

817 self._instances.add(self) 

818 assert len(HistoryManager._instances) <= HistoryManager._max_inst, ( 

819 len(HistoryManager._instances), 

820 HistoryManager._max_inst, 

821 ) 

822 

823 def _switch_to_memory_history(self) -> None: 

824 """Switch history storage to an in-memory SQLite database.""" 

825 try: 

826 self.db.close() 

827 except Exception: 

828 pass 

829 self.hist_file = ":memory:" 

830 self.init_db() 

831 self.new_session() 

832 

833 def _stop_save_thread(self) -> None: 

834 """Stop the background saving thread, if one is running. 

835 

836 The thread closes its own database connection as it exits, so this is 

837 also what releases that connection. 

838 """ 

839 if self.save_thread is not None: 

840 self.save_thread.stop() 

841 self.save_thread = None 

842 

843 def close(self) -> None: 

844 """Stop the saving thread and close the database connection. 

845 

846 This is the deterministic counterpart to relying on garbage 

847 collection: it shuts the saving thread down (which closes its private 

848 connection) and then closes the manager's own connection. Safe to call 

849 more than once. 

850 """ 

851 self._stop_save_thread() 

852 super().close() 

853 

854 def __del__(self) -> None: 

855 self._stop_save_thread() 

856 

857 @classmethod 

858 def _stop_thread(cls) -> None: 

859 # Used before forking so the thread isn't running at fork 

860 for inst in cls._instances: 

861 inst._stop_save_thread() 

862 

863 def _restart_thread_if_stopped(self) -> None: 

864 # Start the thread again after it was stopped for forking 

865 if self.save_thread is None and self.using_thread: 

866 self.save_thread = HistorySavingThread(self) 

867 self.save_thread.start() 

868 

869 def _get_hist_file_name(self, profile: str | None = None) -> Path: 

870 """Get default history file name based on the Shell's profile. 

871 

872 The profile parameter is ignored, but must exist for compatibility with 

873 the parent class.""" 

874 profile_dir = self.shell.profile_dir.location 

875 return Path(profile_dir) / "history.sqlite" 

876 

877 @only_when_enabled 

878 def new_session(self, conn: sqlite3.Connection | None = None) -> None: 

879 """Get a new session number.""" 

880 if conn is None: 

881 conn = self.db 

882 

883 with conn: 

884 cur = conn.execute( 

885 """INSERT INTO sessions VALUES (NULL, ?, NULL, 

886 NULL, '') """, 

887 (datetime.datetime.now().isoformat(" "),), 

888 ) 

889 assert isinstance(cur.lastrowid, int) 

890 self.session_number = cur.lastrowid 

891 

892 def end_session(self) -> None: 

893 """Close the database session, filling in the end time and line count.""" 

894 self.writeout_cache() 

895 with self.db: 

896 self.db.execute( 

897 """UPDATE sessions SET end=?, num_cmds=? WHERE 

898 session==?""", 

899 ( 

900 datetime.datetime.now(datetime.UTC).isoformat(" "), 

901 len(self.input_hist_parsed) - 1, 

902 self.session_number, 

903 ), 

904 ) 

905 self.session_number = 0 

906 

907 def name_session(self, name: str) -> None: 

908 """Give the current session a name in the history database.""" 

909 warn( 

910 "name_session is deprecated in IPython 9.0 and will be removed in future versions", 

911 DeprecationWarning, 

912 stacklevel=2, 

913 ) 

914 with self.db: 

915 self.db.execute( 

916 "UPDATE sessions SET remark=? WHERE session==?", 

917 (name, self.session_number), 

918 ) 

919 

920 def reset(self, new_session: bool = True) -> None: 

921 """Clear the session history, releasing all object references, and 

922 optionally open a new session.""" 

923 self.output_hist.clear() 

924 self.outputs.clear() 

925 self.exceptions.clear() 

926 

927 # The directory history can't be completely empty 

928 self.dir_hist[:] = [Path.cwd()] 

929 

930 if new_session: 

931 if self.session_number: 

932 self.end_session() 

933 self.input_hist_parsed[:] = [""] 

934 self.input_hist_raw[:] = [""] 

935 self.new_session() 

936 

937 # ------------------------------ 

938 # Methods for retrieving history 

939 # ------------------------------ 

940 def get_session_info( 

941 self, session: int = 0 

942 ) -> tuple[int, datetime.datetime, datetime.datetime | None, int | None, str]: 

943 """Get info about a session. 

944 

945 Parameters 

946 ---------- 

947 session : int 

948 Session number to retrieve. The current session is 0, and negative 

949 numbers count back from current session, so -1 is the previous session. 

950 

951 Returns 

952 ------- 

953 session_id : int 

954 Session ID number 

955 start : datetime 

956 Timestamp for the start of the session. 

957 end : datetime 

958 Timestamp for the end of the session, or None if IPython crashed. 

959 num_cmds : int 

960 Number of commands run, or None if IPython crashed. 

961 remark : str 

962 A manually set description. 

963 """ 

964 if session <= 0: 

965 session += self.session_number 

966 

967 return super().get_session_info(session=session) 

968 

969 @catch_corrupt_db 

970 def get_tail( 

971 self, 

972 n: int | None = 10, 

973 raw: bool = True, 

974 output: bool = False, 

975 include_latest: bool = False, 

976 ) -> Iterable[tuple[int, int, InOrInOut]]: 

977 """Get the last n lines from the history database. 

978 

979 Most recent entry last. 

980 

981 Completion will be reordered so that that the last ones are when 

982 possible from current session. 

983 

984 Parameters 

985 ---------- 

986 n : int or None 

987 The number of lines to get. None gets the default of 10, which a 

988 kernel ``history_request`` without ``n`` asks for. 

989 raw, output : bool 

990 See :meth:`get_range` 

991 include_latest : bool 

992 If False (default), n+1 lines are fetched, and the latest one 

993 is discarded. This is intended to be used where the function 

994 is called by a user command, which it should not return. 

995 

996 Returns 

997 ------- 

998 Tuples as :meth:`get_range` 

999 """ 

1000 if n is None: 

1001 n = 10 

1002 self.writeout_cache() 

1003 if not include_latest: 

1004 n += 1 

1005 # cursor/line/entry 

1006 this_cur = list( 

1007 self._run_sql( 

1008 "WHERE session == ? ORDER BY line DESC LIMIT ? ", 

1009 (self.session_number, n), 

1010 raw=raw, 

1011 output=output, 

1012 ) 

1013 ) 

1014 other_cur = list( 

1015 self._run_sql( 

1016 "WHERE session != ? ORDER BY session DESC, line DESC LIMIT ?", 

1017 (self.session_number, n), 

1018 raw=raw, 

1019 output=output, 

1020 ) 

1021 ) 

1022 

1023 everything: list[tuple[int, int, InOrInOut]] = this_cur + other_cur 

1024 

1025 everything = everything[:n] 

1026 

1027 if not include_latest: 

1028 return list(everything)[:0:-1] 

1029 return list(everything)[::-1] 

1030 

1031 def _get_range_session( 

1032 self, 

1033 start: int = 1, 

1034 stop: int | None = None, 

1035 raw: bool = True, 

1036 output: bool = False, 

1037 ) -> Iterable[tuple[int, int, InOrInOut]]: 

1038 """Get input and output history from the current session. Called by 

1039 get_range, and takes similar parameters.""" 

1040 input_hist = self.input_hist_raw if raw else self.input_hist_parsed 

1041 

1042 n = len(input_hist) 

1043 if start < 0: 

1044 start += n 

1045 if not stop or (stop > n): 

1046 stop = n 

1047 elif stop < 0: 

1048 stop += n 

1049 line: InOrInOut 

1050 for i in range(start, stop): 

1051 if output: 

1052 line = (input_hist[i], self.output_hist_reprs.get(i)) 

1053 else: 

1054 line = input_hist[i] 

1055 yield (0, i, line) 

1056 

1057 def get_range( 

1058 self, 

1059 session: int = 0, 

1060 start: int = 1, 

1061 stop: int | None = None, 

1062 raw: bool = True, 

1063 output: bool = False, 

1064 ) -> Iterable[tuple[int, int, InOrInOut]]: 

1065 """Retrieve input by session. 

1066 

1067 Parameters 

1068 ---------- 

1069 session : int 

1070 Session number to retrieve. The current session is 0, and negative 

1071 numbers count back from current session, so -1 is previous session. 

1072 start : int 

1073 First line to retrieve. 

1074 stop : int 

1075 End of line range (excluded from output itself). If None, retrieve 

1076 to the end of the session. 

1077 raw : bool 

1078 If True, return untranslated input 

1079 output : bool 

1080 If True, attempt to include output. This will be 'real' Python 

1081 objects for the current session, or text reprs from previous 

1082 sessions if db_log_output was enabled at the time. Where no output 

1083 is found, None is used. 

1084 

1085 Returns 

1086 ------- 

1087 entries 

1088 An iterator over the desired lines. Each line is a 3-tuple, either 

1089 (session, line, input) if output is False, or 

1090 (session, line, (input, output)) if output is True. 

1091 """ 

1092 if session <= 0: 

1093 session += self.session_number 

1094 if session == self.session_number: # Current session 

1095 return self._get_range_session(start, stop, raw, output) 

1096 return super().get_range(session, start, stop, raw, output) 

1097 

1098 ## ---------------------------- 

1099 ## Methods for storing history: 

1100 ## ---------------------------- 

1101 def store_inputs( 

1102 self, line_num: int, source: str, source_raw: str | None = None 

1103 ) -> None: 

1104 """Store source and raw input in history and create input cache 

1105 variables ``_i*``. 

1106 

1107 Parameters 

1108 ---------- 

1109 line_num : int 

1110 The prompt number of this input. 

1111 source : str 

1112 Python input. 

1113 source_raw : str, optional 

1114 If given, this is the raw input without any IPython transformations 

1115 applied to it. If not given, ``source`` is used. 

1116 """ 

1117 if source_raw is None: 

1118 source_raw = source 

1119 source = source.rstrip("\n") 

1120 source_raw = source_raw.rstrip("\n") 

1121 

1122 # do not store exit/quit commands 

1123 if self._exit_re.match(source_raw.strip()): 

1124 return 

1125 

1126 self.input_hist_parsed.append(source) 

1127 self.input_hist_raw.append(source_raw) 

1128 

1129 with self.db_input_cache_lock: 

1130 self.db_input_cache.append((line_num, source, source_raw)) 

1131 # Trigger to flush cache and write to DB. 

1132 if len(self.db_input_cache) >= self.db_cache_size: 

1133 if self.using_thread: 

1134 self._restart_thread_if_stopped() 

1135 if self.save_flag is not None: 

1136 self.save_flag.set() 

1137 

1138 # update the auto _i variables 

1139 self._iii = self._ii 

1140 self._ii = self._i 

1141 self._i = self._i00 

1142 self._i00 = source_raw 

1143 

1144 # hackish access to user namespace to create _i1,_i2... dynamically 

1145 new_i = "_i%s" % line_num 

1146 to_main = {"_i": self._i, "_ii": self._ii, "_iii": self._iii, new_i: self._i00} 

1147 

1148 if self.shell is not None: 

1149 self.shell.push(to_main, interactive=False) 

1150 

1151 def store_output(self, line_num: int) -> None: 

1152 """If database output logging is enabled, this saves all the 

1153 outputs from the indicated prompt number to the database. It's 

1154 called by run_cell after code has been executed. 

1155 

1156 Parameters 

1157 ---------- 

1158 line_num : int 

1159 The line number from which to save outputs 

1160 """ 

1161 if (not self.db_log_output) or (line_num not in self.output_hist_reprs): 

1162 return 

1163 lnum: int = line_num 

1164 output = self.output_hist_reprs[line_num] 

1165 

1166 with self.db_output_cache_lock: 

1167 self.db_output_cache.append((line_num, output)) 

1168 if self.db_cache_size <= 1 and self.using_thread: 

1169 self._restart_thread_if_stopped() 

1170 if self.save_flag is not None: 

1171 self.save_flag.set() 

1172 

1173 def _writeout_input_cache(self, conn: sqlite3.Connection) -> None: 

1174 with conn: 

1175 for line in self.db_input_cache: 

1176 conn.execute( 

1177 "INSERT INTO history VALUES (?, ?, ?, ?)", 

1178 (self.session_number,) + line, 

1179 ) 

1180 

1181 def _writeout_output_cache(self, conn: sqlite3.Connection) -> None: 

1182 with conn: 

1183 for line in self.db_output_cache: 

1184 conn.execute( 

1185 "INSERT INTO output_history VALUES (?, ?, ?)", 

1186 (self.session_number,) + line, 

1187 ) 

1188 

1189 @only_when_enabled 

1190 def writeout_cache(self, conn: sqlite3.Connection | None = None) -> None: 

1191 """Write any entries in the cache to the database.""" 

1192 if conn is None: 

1193 conn = self.db 

1194 

1195 with self.db_input_cache_lock: 

1196 try: 

1197 self._writeout_input_cache(conn) 

1198 except _sqlite3().IntegrityError: 

1199 self.new_session(conn) 

1200 print( 

1201 "ERROR! Session/line number was not unique in", 

1202 "database. History logging moved to new session", 

1203 self.session_number, 

1204 ) 

1205 try: 

1206 # Try writing to the new session. If this fails, don't 

1207 # recurse 

1208 self._writeout_input_cache(conn) 

1209 except _sqlite3().IntegrityError: 

1210 pass 

1211 finally: 

1212 self.db_input_cache = [] 

1213 

1214 with self.db_output_cache_lock: 

1215 try: 

1216 self._writeout_output_cache(conn) 

1217 except _sqlite3().IntegrityError: 

1218 print( 

1219 "!! Session/line number for output was not unique", 

1220 "in database. Output will not be stored.", 

1221 ) 

1222 finally: 

1223 self.db_output_cache = [] 

1224 

1225 

1226if hasattr(os, "register_at_fork"): 

1227 os.register_at_fork(before=HistoryManager._stop_thread) 

1228 

1229 

1230 

1231@contextmanager 

1232def hold(ref: ReferenceType[HistoryManager]) -> Iterator[ReferenceType[HistoryManager]]: 

1233 """ 

1234 Context manger that hold a reference to a weak ref to make sure it 

1235 is not GC'd during it's context. 

1236 """ 

1237 r = ref() 

1238 yield ref 

1239 del r 

1240 

1241 

1242class HistorySavingThread(threading.Thread): 

1243 """This thread takes care of writing history to the database, so that 

1244 the UI isn't held up while that happens. 

1245 

1246 It waits for the HistoryManager's save_flag to be set, then writes out 

1247 the history cache. The main thread is responsible for setting the flag when 

1248 the cache size reaches a defined threshold.""" 

1249 

1250 save_flag: threading.Event 

1251 daemon: bool = True 

1252 _stop_now: bool = False 

1253 enabled: bool = True 

1254 history_manager: ref[HistoryManager] 

1255 _stopped = False 

1256 db: sqlite3.Connection | None = None 

1257 

1258 def __init__(self, history_manager: HistoryManager) -> None: 

1259 super().__init__(name="IPythonHistorySavingThread") 

1260 self.history_manager = ref(history_manager) 

1261 self.enabled = history_manager.enabled 

1262 self.save_flag = threading.Event() 

1263 

1264 @only_when_enabled 

1265 def run(self) -> None: 

1266 atexit.register(self.stop) 

1267 # We need a separate db connection per thread: 

1268 self.db = None 

1269 try: 

1270 hm: ReferenceType[HistoryManager] 

1271 with hold(self.history_manager) as hm: 

1272 if hm() is not None: 

1273 self.db = _sqlite3().connect( 

1274 str(hm().hist_file), # type: ignore [union-attr] 

1275 **cast(dict[str, t.Any], hm().connection_options), # type: ignore [union-attr] 

1276 ) 

1277 while True: 

1278 self.save_flag.wait() 

1279 with hold(self.history_manager) as hm: 

1280 if hm() is None: 

1281 self._stop_now = True 

1282 if self._stop_now: 

1283 return 

1284 self.save_flag.clear() 

1285 if hm() is not None and self.db is not None: 

1286 hm().writeout_cache(self.db) # type: ignore [union-attr] 

1287 

1288 except Exception as e: 

1289 print( 

1290 ( 

1291 "The history saving thread hit an unexpected error (%s)." 

1292 "History will not be written to the database." 

1293 ) 

1294 % repr(e) 

1295 ) 

1296 finally: 

1297 # Always close our per-thread connection, whatever path we exit by 

1298 # (normal stop, a dropped HistoryManager, or an unexpected error). 

1299 # Leaving it open lets the sqlite3.Connection be garbage collected 

1300 # unclosed, which raises a spurious ``ResourceWarning`` in whatever 

1301 # code happens to be running when the collection occurs. 

1302 if self.db is not None: 

1303 self.db.close() 

1304 self.db = None 

1305 atexit.unregister(self.stop) 

1306 

1307 def stop(self) -> None: 

1308 """This can be called from the main thread to safely stop this thread. 

1309 

1310 Note that it does not attempt to write out remaining history before 

1311 exiting. That should be done by calling the HistoryManager's 

1312 end_session method.""" 

1313 if self._stopped: 

1314 return 

1315 self._stop_now = True 

1316 

1317 self.save_flag.set() 

1318 self._stopped = True 

1319 if self.ident is not None and self != threading.current_thread(): 

1320 self.join() 

1321 

1322 def __del__(self) -> None: 

1323 self.stop() 

1324 

1325 

1326# To match, e.g. ~5/8-~2/3, or ~4 (without trailing slash for full session) 

1327# Session numbers: ~N or N/ 

1328# Line numbers: N (just digits, no ~) 

1329# Range syntax: 4-6 (with end) or 4- (without end, means "onward") 

1330range_re = re.compile( 

1331 r""" 

1332((?P<startsess>(?:~?\d+/)))? 

1333(?P<start>\d+)? 

1334((?P<sep>[\-:]) 

1335 ((?P<endsess>(?:~?\d+/)))? 

1336 (?P<end>\d*))? 

1337$""", 

1338 re.VERBOSE, 

1339) 

1340 

1341 

1342def extract_hist_ranges(ranges_str: str) -> Iterable[tuple[int, int, int | None]]: 

1343 """Turn a string of history ranges into 3-tuples of (session, start, stop). 

1344 

1345 Empty string results in a `[(0, 1, None)]`, i.e. "everything from current 

1346 session". 

1347 

1348 Examples 

1349 -------- 

1350 >>> list(extract_hist_ranges("~8/5-~7/4 2")) 

1351 [(-8, 5, None), (-7, 1, 5), (0, 2, 3)] 

1352 >>> list(extract_hist_ranges("~4/")) 

1353 [(-4, 1, None)] 

1354 >>> list(extract_hist_ranges("4-")) 

1355 [(0, 4, None)] 

1356 >>> list(extract_hist_ranges("~4/4-")) 

1357 [(-4, 4, None)] 

1358 """ 

1359 if ranges_str == "": 

1360 yield (0, 1, None) # Everything from current session 

1361 return 

1362 

1363 for range_str in ranges_str.split(): 

1364 rmatch = range_re.match(range_str) 

1365 if not rmatch: 

1366 continue 

1367 start = rmatch.group("start") 

1368 sep = rmatch.group("sep") 

1369 if start: 

1370 start = int(start) 

1371 end = rmatch.group("end") 

1372 if sep == "-": 

1373 end = (int(end) + 1) if end else None 

1374 else: 

1375 end = int(end) if end else start + 1 

1376 else: 

1377 if not rmatch.group("startsess"): 

1378 continue 

1379 start = 1 

1380 end = None 

1381 startsess = rmatch.group("startsess") or "0" 

1382 endsess = rmatch.group("endsess") or startsess 

1383 startsess = startsess.rstrip("/") 

1384 endsess = endsess.rstrip("/") 

1385 startsess = int(startsess.replace("~", "-")) 

1386 endsess = int(endsess.replace("~", "-")) 

1387 assert endsess >= startsess, "start session must be earlier than end session" 

1388 

1389 if endsess == startsess: 

1390 yield (startsess, start, end) 

1391 continue 

1392 # Multiple sessions in one range: 

1393 yield (startsess, start, None) 

1394 for sess in range(startsess + 1, endsess): 

1395 yield (sess, 1, None) 

1396 yield (endsess, 1, end) 

1397 

1398 

1399def _format_lineno(session: int, line: int) -> str: 

1400 """Helper function to format line numbers properly.""" 

1401 if session == 0: 

1402 return str(line) 

1403 return "{}#{}".format(session, line)