|
20 | 20 | import warnings
|
21 | 21 | from dis import COMPILER_FLAG_NAMES
|
22 | 22 | from pathlib import Path
|
23 |
| -from typing import Any, Callable, ContextManager, Iterable, Sequence |
| 23 | +from typing import Any, Callable, ContextManager, Iterable, NoReturn, Sequence |
24 | 24 |
|
25 | 25 | from prompt_toolkit.formatted_text import OneStyleAndTextTuple
|
26 | 26 | from prompt_toolkit.patch_stdout import patch_stdout as patch_stdout_context
|
|
40 | 40 | except ImportError:
|
41 | 41 | PyCF_ALLOW_TOP_LEVEL_AWAIT = 0
|
42 | 42 |
|
43 |
| -__all__ = ["PythonRepl", "enable_deprecation_warnings", "run_config", "embed"] |
| 43 | + |
| 44 | +__all__ = [ |
| 45 | + "PythonRepl", |
| 46 | + "enable_deprecation_warnings", |
| 47 | + "run_config", |
| 48 | + "embed", |
| 49 | + "exit", |
| 50 | + "ReplExit", |
| 51 | +] |
44 | 52 |
|
45 | 53 |
|
46 | 54 | def _get_coroutine_flag() -> int | None:
|
@@ -91,9 +99,16 @@ def run_and_show_expression(self, expression: str) -> None:
|
91 | 99 | raise
|
92 | 100 | except SystemExit:
|
93 | 101 | raise
|
| 102 | + except ReplExit: |
| 103 | + raise |
94 | 104 | except BaseException as e:
|
95 | 105 | self._handle_exception(e)
|
96 | 106 | else:
|
| 107 | + if isinstance(result, exit): |
| 108 | + # When `exit` is evaluated without parentheses. |
| 109 | + # Automatically trigger the `ReplExit` exception. |
| 110 | + raise ReplExit |
| 111 | + |
97 | 112 | # Print.
|
98 | 113 | if result is not None:
|
99 | 114 | self._show_result(result)
|
@@ -155,7 +170,10 @@ def run(self) -> None:
|
155 | 170 | continue
|
156 | 171 |
|
157 | 172 | # Run it; display the result (or errors if applicable).
|
158 |
| - self.run_and_show_expression(text) |
| 173 | + try: |
| 174 | + self.run_and_show_expression(text) |
| 175 | + except ReplExit: |
| 176 | + return |
159 | 177 | finally:
|
160 | 178 | if self.terminal_title:
|
161 | 179 | clear_title()
|
@@ -383,6 +401,7 @@ def get_ptpython() -> PythonInput:
|
383 | 401 | return self
|
384 | 402 |
|
385 | 403 | globals["get_ptpython"] = get_ptpython
|
| 404 | + globals["exit"] = exit() |
386 | 405 |
|
387 | 406 | def _remove_from_namespace(self) -> None:
|
388 | 407 | """
|
@@ -459,6 +478,29 @@ def enter_to_continue() -> None:
|
459 | 478 | enter_to_continue()
|
460 | 479 |
|
461 | 480 |
|
| 481 | +class exit: |
| 482 | + """ |
| 483 | + Exit the ptpython REPL. |
| 484 | + """ |
| 485 | + |
| 486 | + # This custom exit function ensures that the `embed` function returns from |
| 487 | + # where we are embedded, and Python doesn't close `sys.stdin` like |
| 488 | + # the default `exit` from `_sitebuiltins.Quitter` does. |
| 489 | + |
| 490 | + def __call__(self) -> NoReturn: |
| 491 | + raise ReplExit |
| 492 | + |
| 493 | + def __repr__(self) -> str: |
| 494 | + # (Same message as the built-in Python REPL.) |
| 495 | + return "Use exit() or Ctrl-D (i.e. EOF) to exit" |
| 496 | + |
| 497 | + |
| 498 | +class ReplExit(Exception): |
| 499 | + """ |
| 500 | + Exception raised by ptpython's exit function. |
| 501 | + """ |
| 502 | + |
| 503 | + |
462 | 504 | def embed(
|
463 | 505 | globals: dict[str, Any] | None = None,
|
464 | 506 | locals: dict[str, Any] | None = None,
|
|
0 commit comments