Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/jupyter_client/client.py: 24%

332 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-01 06:54 +0000

1"""Base class to manage the interaction with a running kernel""" 

2# Copyright (c) Jupyter Development Team. 

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

4import asyncio 

5import inspect 

6import sys 

7import time 

8import typing as t 

9from functools import partial 

10from getpass import getpass 

11from queue import Empty 

12 

13import zmq.asyncio 

14from jupyter_core.utils import ensure_async 

15from traitlets import Any, Bool, Instance, Type 

16 

17from .channels import major_protocol_version 

18from .channelsabc import ChannelABC, HBChannelABC 

19from .clientabc import KernelClientABC 

20from .connect import ConnectionFileMixin 

21from .session import Session 

22 

23# some utilities to validate message structure, these might get moved elsewhere 

24# if they prove to have more generic utility 

25 

26 

27def validate_string_dict(dct: t.Dict[str, str]) -> None: 

28 """Validate that the input is a dict with string keys and values. 

29 

30 Raises ValueError if not.""" 

31 for k, v in dct.items(): 

32 if not isinstance(k, str): 

33 raise ValueError("key %r in dict must be a string" % k) 

34 if not isinstance(v, str): 

35 raise ValueError("value %r in dict must be a string" % v) 

36 

37 

38def reqrep(wrapped: t.Callable, meth: t.Callable, channel: str = "shell") -> t.Callable: 

39 wrapped = wrapped(meth, channel) 

40 if not meth.__doc__: 

41 # python -OO removes docstrings, 

42 # so don't bother building the wrapped docstring 

43 return wrapped 

44 

45 basedoc, _ = meth.__doc__.split("Returns\n", 1) 

46 parts = [basedoc.strip()] 

47 if "Parameters" not in basedoc: 

48 parts.append( 

49 """ 

50 Parameters 

51 ---------- 

52 """ 

53 ) 

54 parts.append( 

55 """ 

56 reply: bool (default: False) 

57 Whether to wait for and return reply 

58 timeout: float or None (default: None) 

59 Timeout to use when waiting for a reply 

60 

61 Returns 

62 ------- 

63 msg_id: str 

64 The msg_id of the request sent, if reply=False (default) 

65 reply: dict 

66 The reply message for this request, if reply=True 

67 """ 

68 ) 

69 wrapped.__doc__ = "\n".join(parts) 

70 return wrapped 

71 

72 

73class KernelClient(ConnectionFileMixin): 

74 """Communicates with a single kernel on any host via zmq channels. 

75 

76 There are five channels associated with each kernel: 

77 

78 * shell: for request/reply calls to the kernel. 

79 * iopub: for the kernel to publish results to frontends. 

80 * hb: for monitoring the kernel's heartbeat. 

81 * stdin: for frontends to reply to raw_input calls in the kernel. 

82 * control: for kernel management calls to the kernel. 

83 

84 The messages that can be sent on these channels are exposed as methods of the 

85 client (KernelClient.execute, complete, history, etc.). These methods only 

86 send the message, they don't wait for a reply. To get results, use e.g. 

87 :meth:`get_shell_msg` to fetch messages from the shell channel. 

88 """ 

89 

90 # The PyZMQ Context to use for communication with the kernel. 

91 context = Instance(zmq.Context) 

92 

93 _created_context = Bool(False) 

94 

95 def _context_default(self) -> zmq.Context: 

96 self._created_context = True 

97 return zmq.Context() 

98 

99 # The classes to use for the various channels 

100 shell_channel_class = Type(ChannelABC) 

101 iopub_channel_class = Type(ChannelABC) 

102 stdin_channel_class = Type(ChannelABC) 

103 hb_channel_class = Type(HBChannelABC) 

104 control_channel_class = Type(ChannelABC) 

105 

106 # Protected traits 

107 _shell_channel = Any() 

108 _iopub_channel = Any() 

109 _stdin_channel = Any() 

110 _hb_channel = Any() 

111 _control_channel = Any() 

112 

113 # flag for whether execute requests should be allowed to call raw_input: 

114 allow_stdin: bool = True 

115 

116 def __del__(self): 

117 """Handle garbage collection. Destroy context if applicable.""" 

118 if self._created_context and self.context and not self.context.closed: 

119 if self.channels_running: 

120 if self.log: 

121 self.log.warning("Could not destroy zmq context for %s", self) 

122 else: 

123 if self.log: 

124 self.log.debug("Destroying zmq context for %s", self) 

125 self.context.destroy() 

126 try: 

127 super_del = super().__del__ # type:ignore[misc] 

128 except AttributeError: 

129 pass 

130 else: 

131 super_del() 

132 

133 # -------------------------------------------------------------------------- 

134 # Channel proxy methods 

135 # -------------------------------------------------------------------------- 

136 

137 async def _async_get_shell_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]: 

138 """Get a message from the shell channel""" 

139 return await ensure_async(self.shell_channel.get_msg(*args, **kwargs)) 

140 

141 async def _async_get_iopub_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]: 

142 """Get a message from the iopub channel""" 

143 return await ensure_async(self.iopub_channel.get_msg(*args, **kwargs)) 

144 

145 async def _async_get_stdin_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]: 

146 """Get a message from the stdin channel""" 

147 return await ensure_async(self.stdin_channel.get_msg(*args, **kwargs)) 

148 

149 async def _async_get_control_msg(self, *args: t.Any, **kwargs: t.Any) -> t.Dict[str, t.Any]: 

150 """Get a message from the control channel""" 

151 return await ensure_async(self.control_channel.get_msg(*args, **kwargs)) 

152 

153 async def _async_wait_for_ready(self, timeout: t.Optional[float] = None) -> None: 

154 """Waits for a response when a client is blocked 

155 

156 - Sets future time for timeout 

157 - Blocks on shell channel until a message is received 

158 - Exit if the kernel has died 

159 - If client times out before receiving a message from the kernel, send RuntimeError 

160 - Flush the IOPub channel 

161 """ 

162 if timeout is None: 

163 timeout = float("inf") 

164 abs_timeout = time.time() + timeout 

165 

166 from .manager import KernelManager 

167 

168 if not isinstance(self.parent, KernelManager): 

169 # This Client was not created by a KernelManager, 

170 # so wait for kernel to become responsive to heartbeats 

171 # before checking for kernel_info reply 

172 while not await self._async_is_alive(): 

173 if time.time() > abs_timeout: 

174 raise RuntimeError( 

175 "Kernel didn't respond to heartbeats in %d seconds and timed out" % timeout 

176 ) 

177 await asyncio.sleep(0.2) 

178 

179 # Wait for kernel info reply on shell channel 

180 while True: 

181 self.kernel_info() 

182 try: 

183 msg = await ensure_async(self.shell_channel.get_msg(timeout=1)) 

184 except Empty: 

185 pass 

186 else: 

187 if msg["msg_type"] == "kernel_info_reply": 

188 # Checking that IOPub is connected. If it is not connected, start over. 

189 try: 

190 await ensure_async(self.iopub_channel.get_msg(timeout=0.2)) 

191 except Empty: 

192 pass 

193 else: 

194 self._handle_kernel_info_reply(msg) 

195 break 

196 

197 if not await self._async_is_alive(): 

198 msg = "Kernel died before replying to kernel_info" 

199 raise RuntimeError(msg) 

200 

201 # Check if current time is ready check time plus timeout 

202 if time.time() > abs_timeout: 

203 raise RuntimeError("Kernel didn't respond in %d seconds" % timeout) 

204 

205 # Flush IOPub channel 

206 while True: 

207 try: 

208 msg = await ensure_async(self.iopub_channel.get_msg(timeout=0.2)) 

209 except Empty: 

210 break 

211 

212 async def _async_recv_reply( 

213 self, msg_id: str, timeout: t.Optional[float] = None, channel: str = "shell" 

214 ) -> t.Dict[str, t.Any]: 

215 """Receive and return the reply for a given request""" 

216 if timeout is not None: 

217 deadline = time.monotonic() + timeout 

218 while True: 

219 if timeout is not None: 

220 timeout = max(0, deadline - time.monotonic()) 

221 try: 

222 if channel == "control": 

223 reply = await self._async_get_control_msg(timeout=timeout) 

224 else: 

225 reply = await self._async_get_shell_msg(timeout=timeout) 

226 except Empty as e: 

227 msg = "Timeout waiting for reply" 

228 raise TimeoutError(msg) from e 

229 if reply["parent_header"].get("msg_id") != msg_id: 

230 # not my reply, someone may have forgotten to retrieve theirs 

231 continue 

232 return reply 

233 

234 async def _stdin_hook_default(self, msg: t.Dict[str, t.Any]) -> None: 

235 """Handle an input request""" 

236 content = msg["content"] 

237 prompt = getpass if content.get("password", False) else input 

238 

239 try: 

240 raw_data = prompt(content["prompt"]) # type:ignore[operator] 

241 except EOFError: 

242 # turn EOFError into EOF character 

243 raw_data = "\x04" 

244 except KeyboardInterrupt: 

245 sys.stdout.write("\n") 

246 return 

247 

248 # only send stdin reply if there *was not* another request 

249 # or execution finished while we were reading. 

250 if not (await self.stdin_channel.msg_ready() or await self.shell_channel.msg_ready()): 

251 self.input(raw_data) 

252 

253 def _output_hook_default(self, msg: t.Dict[str, t.Any]) -> None: 

254 """Default hook for redisplaying plain-text output""" 

255 msg_type = msg["header"]["msg_type"] 

256 content = msg["content"] 

257 if msg_type == "stream": 

258 stream = getattr(sys, content["name"]) 

259 stream.write(content["text"]) 

260 elif msg_type in ("display_data", "execute_result"): 

261 sys.stdout.write(content["data"].get("text/plain", "")) 

262 elif msg_type == "error": 

263 sys.stderr.write("\n".join(content["traceback"])) 

264 

265 def _output_hook_kernel( 

266 self, 

267 session: Session, 

268 socket: zmq.sugar.socket.Socket, 

269 parent_header: t.Any, 

270 msg: t.Dict[str, t.Any], 

271 ) -> None: 

272 """Output hook when running inside an IPython kernel 

273 

274 adds rich output support. 

275 """ 

276 msg_type = msg["header"]["msg_type"] 

277 if msg_type in ("display_data", "execute_result", "error"): 

278 session.send(socket, msg_type, msg["content"], parent=parent_header) 

279 else: 

280 self._output_hook_default(msg) 

281 

282 # -------------------------------------------------------------------------- 

283 # Channel management methods 

284 # -------------------------------------------------------------------------- 

285 

286 def start_channels( 

287 self, 

288 shell: bool = True, 

289 iopub: bool = True, 

290 stdin: bool = True, 

291 hb: bool = True, 

292 control: bool = True, 

293 ) -> None: 

294 """Starts the channels for this kernel. 

295 

296 This will create the channels if they do not exist and then start 

297 them (their activity runs in a thread). If port numbers of 0 are 

298 being used (random ports) then you must first call 

299 :meth:`start_kernel`. If the channels have been stopped and you 

300 call this, :class:`RuntimeError` will be raised. 

301 """ 

302 if iopub: 

303 self.iopub_channel.start() 

304 if shell: 

305 self.shell_channel.start() 

306 if stdin: 

307 self.stdin_channel.start() 

308 self.allow_stdin = True 

309 else: 

310 self.allow_stdin = False 

311 if hb: 

312 self.hb_channel.start() 

313 if control: 

314 self.control_channel.start() 

315 

316 def stop_channels(self) -> None: 

317 """Stops all the running channels for this kernel. 

318 

319 This stops their event loops and joins their threads. 

320 """ 

321 if self.shell_channel.is_alive(): 

322 self.shell_channel.stop() 

323 if self.iopub_channel.is_alive(): 

324 self.iopub_channel.stop() 

325 if self.stdin_channel.is_alive(): 

326 self.stdin_channel.stop() 

327 if self.hb_channel.is_alive(): 

328 self.hb_channel.stop() 

329 if self.control_channel.is_alive(): 

330 self.control_channel.stop() 

331 

332 @property 

333 def channels_running(self) -> bool: 

334 """Are any of the channels created and running?""" 

335 return ( 

336 (self._shell_channel and self.shell_channel.is_alive()) 

337 or (self._iopub_channel and self.iopub_channel.is_alive()) 

338 or (self._stdin_channel and self.stdin_channel.is_alive()) 

339 or (self._hb_channel and self.hb_channel.is_alive()) 

340 or (self._control_channel and self.control_channel.is_alive()) 

341 ) 

342 

343 ioloop = None # Overridden in subclasses that use pyzmq event loop 

344 

345 @property 

346 def shell_channel(self) -> t.Any: 

347 """Get the shell channel object for this kernel.""" 

348 if self._shell_channel is None: 

349 url = self._make_url("shell") 

350 self.log.debug("connecting shell channel to %s", url) 

351 socket = self.connect_shell(identity=self.session.bsession) 

352 self._shell_channel = self.shell_channel_class(socket, self.session, self.ioloop) 

353 return self._shell_channel 

354 

355 @property 

356 def iopub_channel(self) -> t.Any: 

357 """Get the iopub channel object for this kernel.""" 

358 if self._iopub_channel is None: 

359 url = self._make_url("iopub") 

360 self.log.debug("connecting iopub channel to %s", url) 

361 socket = self.connect_iopub() 

362 self._iopub_channel = self.iopub_channel_class(socket, self.session, self.ioloop) 

363 return self._iopub_channel 

364 

365 @property 

366 def stdin_channel(self) -> t.Any: 

367 """Get the stdin channel object for this kernel.""" 

368 if self._stdin_channel is None: 

369 url = self._make_url("stdin") 

370 self.log.debug("connecting stdin channel to %s", url) 

371 socket = self.connect_stdin(identity=self.session.bsession) 

372 self._stdin_channel = self.stdin_channel_class(socket, self.session, self.ioloop) 

373 return self._stdin_channel 

374 

375 @property 

376 def hb_channel(self) -> t.Any: 

377 """Get the hb channel object for this kernel.""" 

378 if self._hb_channel is None: 

379 url = self._make_url("hb") 

380 self.log.debug("connecting heartbeat channel to %s", url) 

381 self._hb_channel = self.hb_channel_class(self.context, self.session, url) 

382 return self._hb_channel 

383 

384 @property 

385 def control_channel(self) -> t.Any: 

386 """Get the control channel object for this kernel.""" 

387 if self._control_channel is None: 

388 url = self._make_url("control") 

389 self.log.debug("connecting control channel to %s", url) 

390 socket = self.connect_control(identity=self.session.bsession) 

391 self._control_channel = self.control_channel_class(socket, self.session, self.ioloop) 

392 return self._control_channel 

393 

394 async def _async_is_alive(self) -> bool: 

395 """Is the kernel process still running?""" 

396 from .manager import KernelManager 

397 

398 if isinstance(self.parent, KernelManager): 

399 # This KernelClient was created by a KernelManager, 

400 # we can ask the parent KernelManager: 

401 return await self.parent._async_is_alive() 

402 if self._hb_channel is not None: 

403 # We don't have access to the KernelManager, 

404 # so we use the heartbeat. 

405 return self._hb_channel.is_beating() 

406 # no heartbeat and not local, we can't tell if it's running, 

407 # so naively return True 

408 return True 

409 

410 async def _async_execute_interactive( 

411 self, 

412 code: str, 

413 silent: bool = False, 

414 store_history: bool = True, 

415 user_expressions: t.Optional[t.Dict[str, t.Any]] = None, 

416 allow_stdin: t.Optional[bool] = None, 

417 stop_on_error: bool = True, 

418 timeout: t.Optional[float] = None, 

419 output_hook: t.Optional[t.Callable] = None, 

420 stdin_hook: t.Optional[t.Callable] = None, 

421 ) -> t.Dict[str, t.Any]: 

422 """Execute code in the kernel interactively 

423 

424 Output will be redisplayed, and stdin prompts will be relayed as well. 

425 If an IPython kernel is detected, rich output will be displayed. 

426 

427 You can pass a custom output_hook callable that will be called 

428 with every IOPub message that is produced instead of the default redisplay. 

429 

430 .. versionadded:: 5.0 

431 

432 Parameters 

433 ---------- 

434 code : str 

435 A string of code in the kernel's language. 

436 

437 silent : bool, optional (default False) 

438 If set, the kernel will execute the code as quietly possible, and 

439 will force store_history to be False. 

440 

441 store_history : bool, optional (default True) 

442 If set, the kernel will store command history. This is forced 

443 to be False if silent is True. 

444 

445 user_expressions : dict, optional 

446 A dict mapping names to expressions to be evaluated in the user's 

447 dict. The expression values are returned as strings formatted using 

448 :func:`repr`. 

449 

450 allow_stdin : bool, optional (default self.allow_stdin) 

451 Flag for whether the kernel can send stdin requests to frontends. 

452 

453 Some frontends (e.g. the Notebook) do not support stdin requests. 

454 If raw_input is called from code executed from such a frontend, a 

455 StdinNotImplementedError will be raised. 

456 

457 stop_on_error: bool, optional (default True) 

458 Flag whether to abort the execution queue, if an exception is encountered. 

459 

460 timeout: float or None (default: None) 

461 Timeout to use when waiting for a reply 

462 

463 output_hook: callable(msg) 

464 Function to be called with output messages. 

465 If not specified, output will be redisplayed. 

466 

467 stdin_hook: callable(msg) 

468 Function or awaitable to be called with stdin_request messages. 

469 If not specified, input/getpass will be called. 

470 

471 Returns 

472 ------- 

473 reply: dict 

474 The reply message for this request 

475 """ 

476 if not self.iopub_channel.is_alive(): 

477 emsg = "IOPub channel must be running to receive output" 

478 raise RuntimeError(emsg) 

479 if allow_stdin is None: 

480 allow_stdin = self.allow_stdin 

481 if allow_stdin and not self.stdin_channel.is_alive(): 

482 emsg = "stdin channel must be running to allow input" 

483 raise RuntimeError(emsg) 

484 msg_id = await ensure_async( 

485 self.execute( 

486 code, 

487 silent=silent, 

488 store_history=store_history, 

489 user_expressions=user_expressions, 

490 allow_stdin=allow_stdin, 

491 stop_on_error=stop_on_error, 

492 ) 

493 ) 

494 if stdin_hook is None: 

495 stdin_hook = self._stdin_hook_default 

496 # detect IPython kernel 

497 if output_hook is None and "IPython" in sys.modules: 

498 from IPython import get_ipython 

499 

500 ip = get_ipython() 

501 in_kernel = getattr(ip, "kernel", False) 

502 if in_kernel: 

503 output_hook = partial( 

504 self._output_hook_kernel, 

505 ip.display_pub.session, 

506 ip.display_pub.pub_socket, 

507 ip.display_pub.parent_header, 

508 ) 

509 if output_hook is None: 

510 # default: redisplay plain-text outputs 

511 output_hook = self._output_hook_default 

512 

513 # set deadline based on timeout 

514 if timeout is not None: 

515 deadline = time.monotonic() + timeout 

516 else: 

517 timeout_ms = None 

518 

519 poller = zmq.Poller() 

520 iopub_socket = self.iopub_channel.socket 

521 poller.register(iopub_socket, zmq.POLLIN) 

522 if allow_stdin: 

523 stdin_socket = self.stdin_channel.socket 

524 poller.register(stdin_socket, zmq.POLLIN) 

525 else: 

526 stdin_socket = None 

527 

528 # wait for output and redisplay it 

529 while True: 

530 if timeout is not None: 

531 timeout = max(0, deadline - time.monotonic()) 

532 timeout_ms = int(1000 * timeout) 

533 events = dict(poller.poll(timeout_ms)) 

534 if not events: 

535 emsg = "Timeout waiting for output" 

536 raise TimeoutError(emsg) 

537 if stdin_socket in events: 

538 req = await ensure_async(self.stdin_channel.get_msg(timeout=0)) 

539 res = stdin_hook(req) 

540 if inspect.isawaitable(res): 

541 await res 

542 continue 

543 if iopub_socket not in events: 

544 continue 

545 

546 msg = await ensure_async(self.iopub_channel.get_msg(timeout=0)) 

547 

548 if msg["parent_header"].get("msg_id") != msg_id: 

549 # not from my request 

550 continue 

551 output_hook(msg) 

552 

553 # stop on idle 

554 if ( 

555 msg["header"]["msg_type"] == "status" 

556 and msg["content"]["execution_state"] == "idle" 

557 ): 

558 break 

559 

560 # output is done, get the reply 

561 if timeout is not None: 

562 timeout = max(0, deadline - time.monotonic()) 

563 return await self._async_recv_reply(msg_id, timeout=timeout) 

564 

565 # Methods to send specific messages on channels 

566 def execute( 

567 self, 

568 code: str, 

569 silent: bool = False, 

570 store_history: bool = True, 

571 user_expressions: t.Optional[t.Dict[str, t.Any]] = None, 

572 allow_stdin: t.Optional[bool] = None, 

573 stop_on_error: bool = True, 

574 ) -> str: 

575 """Execute code in the kernel. 

576 

577 Parameters 

578 ---------- 

579 code : str 

580 A string of code in the kernel's language. 

581 

582 silent : bool, optional (default False) 

583 If set, the kernel will execute the code as quietly possible, and 

584 will force store_history to be False. 

585 

586 store_history : bool, optional (default True) 

587 If set, the kernel will store command history. This is forced 

588 to be False if silent is True. 

589 

590 user_expressions : dict, optional 

591 A dict mapping names to expressions to be evaluated in the user's 

592 dict. The expression values are returned as strings formatted using 

593 :func:`repr`. 

594 

595 allow_stdin : bool, optional (default self.allow_stdin) 

596 Flag for whether the kernel can send stdin requests to frontends. 

597 

598 Some frontends (e.g. the Notebook) do not support stdin requests. 

599 If raw_input is called from code executed from such a frontend, a 

600 StdinNotImplementedError will be raised. 

601 

602 stop_on_error: bool, optional (default True) 

603 Flag whether to abort the execution queue, if an exception is encountered. 

604 

605 Returns 

606 ------- 

607 The msg_id of the message sent. 

608 """ 

609 if user_expressions is None: 

610 user_expressions = {} 

611 if allow_stdin is None: 

612 allow_stdin = self.allow_stdin 

613 

614 # Don't waste network traffic if inputs are invalid 

615 if not isinstance(code, str): 

616 raise ValueError("code %r must be a string" % code) 

617 validate_string_dict(user_expressions) 

618 

619 # Create class for content/msg creation. Related to, but possibly 

620 # not in Session. 

621 content = { 

622 "code": code, 

623 "silent": silent, 

624 "store_history": store_history, 

625 "user_expressions": user_expressions, 

626 "allow_stdin": allow_stdin, 

627 "stop_on_error": stop_on_error, 

628 } 

629 msg = self.session.msg("execute_request", content) 

630 self.shell_channel.send(msg) 

631 return msg["header"]["msg_id"] 

632 

633 def complete(self, code: str, cursor_pos: t.Optional[int] = None) -> str: 

634 """Tab complete text in the kernel's namespace. 

635 

636 Parameters 

637 ---------- 

638 code : str 

639 The context in which completion is requested. 

640 Can be anything between a variable name and an entire cell. 

641 cursor_pos : int, optional 

642 The position of the cursor in the block of code where the completion was requested. 

643 Default: ``len(code)`` 

644 

645 Returns 

646 ------- 

647 The msg_id of the message sent. 

648 """ 

649 if cursor_pos is None: 

650 cursor_pos = len(code) 

651 content = {"code": code, "cursor_pos": cursor_pos} 

652 msg = self.session.msg("complete_request", content) 

653 self.shell_channel.send(msg) 

654 return msg["header"]["msg_id"] 

655 

656 def inspect(self, code: str, cursor_pos: t.Optional[int] = None, detail_level: int = 0) -> str: 

657 """Get metadata information about an object in the kernel's namespace. 

658 

659 It is up to the kernel to determine the appropriate object to inspect. 

660 

661 Parameters 

662 ---------- 

663 code : str 

664 The context in which info is requested. 

665 Can be anything between a variable name and an entire cell. 

666 cursor_pos : int, optional 

667 The position of the cursor in the block of code where the info was requested. 

668 Default: ``len(code)`` 

669 detail_level : int, optional 

670 The level of detail for the introspection (0-2) 

671 

672 Returns 

673 ------- 

674 The msg_id of the message sent. 

675 """ 

676 if cursor_pos is None: 

677 cursor_pos = len(code) 

678 content = { 

679 "code": code, 

680 "cursor_pos": cursor_pos, 

681 "detail_level": detail_level, 

682 } 

683 msg = self.session.msg("inspect_request", content) 

684 self.shell_channel.send(msg) 

685 return msg["header"]["msg_id"] 

686 

687 def history( 

688 self, 

689 raw: bool = True, 

690 output: bool = False, 

691 hist_access_type: str = "range", 

692 **kwargs: t.Any, 

693 ) -> str: 

694 """Get entries from the kernel's history list. 

695 

696 Parameters 

697 ---------- 

698 raw : bool 

699 If True, return the raw input. 

700 output : bool 

701 If True, then return the output as well. 

702 hist_access_type : str 

703 'range' (fill in session, start and stop params), 'tail' (fill in n) 

704 or 'search' (fill in pattern param). 

705 

706 session : int 

707 For a range request, the session from which to get lines. Session 

708 numbers are positive integers; negative ones count back from the 

709 current session. 

710 start : int 

711 The first line number of a history range. 

712 stop : int 

713 The final (excluded) line number of a history range. 

714 

715 n : int 

716 The number of lines of history to get for a tail request. 

717 

718 pattern : str 

719 The glob-syntax pattern for a search request. 

720 

721 Returns 

722 ------- 

723 The ID of the message sent. 

724 """ 

725 if hist_access_type == "range": 

726 kwargs.setdefault("session", 0) 

727 kwargs.setdefault("start", 0) 

728 content = dict(raw=raw, output=output, hist_access_type=hist_access_type, **kwargs) 

729 msg = self.session.msg("history_request", content) 

730 self.shell_channel.send(msg) 

731 return msg["header"]["msg_id"] 

732 

733 def kernel_info(self) -> str: 

734 """Request kernel info 

735 

736 Returns 

737 ------- 

738 The msg_id of the message sent 

739 """ 

740 msg = self.session.msg("kernel_info_request") 

741 self.shell_channel.send(msg) 

742 return msg["header"]["msg_id"] 

743 

744 def comm_info(self, target_name: t.Optional[str] = None) -> str: 

745 """Request comm info 

746 

747 Returns 

748 ------- 

749 The msg_id of the message sent 

750 """ 

751 content = {} if target_name is None else {"target_name": target_name} 

752 msg = self.session.msg("comm_info_request", content) 

753 self.shell_channel.send(msg) 

754 return msg["header"]["msg_id"] 

755 

756 def _handle_kernel_info_reply(self, msg: t.Dict[str, t.Any]) -> None: 

757 """handle kernel info reply 

758 

759 sets protocol adaptation version. This might 

760 be run from a separate thread. 

761 """ 

762 adapt_version = int(msg["content"]["protocol_version"].split(".")[0]) 

763 if adapt_version != major_protocol_version: 

764 self.session.adapt_version = adapt_version 

765 

766 def is_complete(self, code: str) -> str: 

767 """Ask the kernel whether some code is complete and ready to execute. 

768 

769 Returns 

770 ------- 

771 The ID of the message sent. 

772 """ 

773 msg = self.session.msg("is_complete_request", {"code": code}) 

774 self.shell_channel.send(msg) 

775 return msg["header"]["msg_id"] 

776 

777 def input(self, string: str) -> None: 

778 """Send a string of raw input to the kernel. 

779 

780 This should only be called in response to the kernel sending an 

781 ``input_request`` message on the stdin channel. 

782 

783 Returns 

784 ------- 

785 The ID of the message sent. 

786 """ 

787 content = {"value": string} 

788 msg = self.session.msg("input_reply", content) 

789 self.stdin_channel.send(msg) 

790 

791 def shutdown(self, restart: bool = False) -> str: 

792 """Request an immediate kernel shutdown on the control channel. 

793 

794 Upon receipt of the (empty) reply, client code can safely assume that 

795 the kernel has shut down and it's safe to forcefully terminate it if 

796 it's still alive. 

797 

798 The kernel will send the reply via a function registered with Python's 

799 atexit module, ensuring it's truly done as the kernel is done with all 

800 normal operation. 

801 

802 Returns 

803 ------- 

804 The msg_id of the message sent 

805 """ 

806 # Send quit message to kernel. Once we implement kernel-side setattr, 

807 # this should probably be done that way, but for now this will do. 

808 msg = self.session.msg("shutdown_request", {"restart": restart}) 

809 self.control_channel.send(msg) 

810 return msg["header"]["msg_id"] 

811 

812 

813KernelClientABC.register(KernelClient)