Skip to content

Commit aeb0aaf

Browse files
committed
First cut at adding an interrupt handler
1 parent 13c84f8 commit aeb0aaf

File tree

6 files changed

+355
-60
lines changed

6 files changed

+355
-60
lines changed

mathicsscript/__main__.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,12 @@
2424
from pygments import highlight
2525

2626
from mathicsscript.asymptote import asymptote_version
27+
from mathicsscript.interrupt import setup_signal_handler
2728
from mathicsscript.settings import definitions
2829
from mathicsscript.termshell import ShellEscapeException, mma_lexer
2930
from mathicsscript.termshell_gnu import TerminalShellGNUReadline
30-
from mathicsscript.termshell_prompt import (
31-
TerminalShellCommon,
32-
TerminalShellPromptToolKit,
33-
)
31+
from mathicsscript.termshell import TerminalShellCommon
32+
from mathicsscript.termshell_prompt import TerminalShellPromptToolKit
3433
from mathicsscript.version import __version__
3534

3635
try:
@@ -123,7 +122,7 @@ def load_settings(shell):
123122
continue
124123
evaluation.evaluate(query)
125124
except KeyboardInterrupt:
126-
print("\nKeyboardInterrupt")
125+
shell.errmsg("\nKeyboardInterrupt")
127126
return True
128127

129128

@@ -145,16 +144,17 @@ def interactive_eval_loop(
145144
shell: TerminalShellCommon,
146145
unicode,
147146
prompt,
148-
matplotlib: bool,
149-
asymptote: bool,
150147
strict_wl_output: bool,
151148
):
149+
setup_signal_handler()
150+
152151
def identity(x: Any) -> Any:
153152
return x
154153

155154
def fmt_fun(query: Any) -> Any:
156155
return highlight(str(query), mma_lexer, shell.terminal_formatter)
157156

157+
shell.fmt_fn = fmt_fun
158158
while True:
159159
try:
160160
if have_readline and shell.using_readline:
@@ -173,6 +173,11 @@ def fmt_fun(query: Any) -> Any:
173173
fmt = fmt_fun
174174

175175
evaluation = Evaluation(shell.definitions, output=TerminalOutput(shell))
176+
177+
# Store shell into the evaluation so that an interrupt handler
178+
# has access to this
179+
evaluation.shell = shell
180+
176181
query, source_code = evaluation.parse_feeder_returning_code(shell)
177182
if mathics_core.PRE_EVALUATION_HOOK is not None:
178183
mathics_core.PRE_EVALUATION_HOOK(query, evaluation)
@@ -214,7 +219,7 @@ def fmt_fun(query: Any) -> Any:
214219
try:
215220
print(open(source_code[2:], "r").read())
216221
except Exception:
217-
print(str(sys.exc_info()[1]))
222+
shell.errmsg(str(sys.exc_info()[1]))
218223
else:
219224
subprocess.run(source_code[1:], shell=True)
220225

@@ -224,13 +229,13 @@ def fmt_fun(query: Any) -> Any:
224229
# shell.definitions.increment_line(1)
225230

226231
except KeyboardInterrupt:
227-
print("\nKeyboardInterrupt")
232+
shell.errmsg("\nKeyboardInterrupt")
228233
except EOFError:
229234
if prompt:
230-
print("\n\nGoodbye!\n")
235+
shell.errmsg("\n\nGoodbye!\n")
231236
break
232237
except SystemExit:
233-
print("\n\nGoodbye!\n")
238+
shell.errmsg("\n\nGoodbye!\n")
234239
# raise to pass the error code on, e.g. Quit[1]
235240
raise
236241
finally:
@@ -526,9 +531,7 @@ def main(
526531
)
527532

528533
definitions.set_line_no(0)
529-
interactive_eval_loop(
530-
shell, charset, prompt, asymptote, matplotlib, strict_wl_output
531-
)
534+
interactive_eval_loop(shell, charset, prompt, strict_wl_output)
532535
return exit_rc
533536

534537

mathicsscript/completion.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
from mathics.core.symbols import strip_context
2424
from mathics_scanner import named_characters
2525
from mathics_pygments.lexer import Regex
26-
from prompt_toolkit.completion import CompleteEvent, Completion, WordCompleter
26+
from prompt_toolkit.completion import (
27+
CompleteEvent,
28+
Completer,
29+
Completion,
30+
WordCompleter,
31+
)
2732
from prompt_toolkit.document import Document
2833

2934
SYMBOLS = rf"[`]?({Regex.IDENTIFIER}|{Regex.NAMED_CHARACTER})(`({Regex.IDENTIFIER}|{Regex.NAMED_CHARACTER}))+[`]?"
@@ -54,6 +59,28 @@ def get_datadir():
5459
return osp.realpath(datadir)
5560

5661

62+
class InterruptCompleter(Completer):
63+
"""
64+
Completer for the simple command set: 'continue', 'abort', 'exit', 'show'.
65+
"""
66+
67+
COMMANDS = [
68+
"abort",
69+
"continue",
70+
"exit",
71+
"inspect",
72+
"show",
73+
]
74+
75+
def get_completions(
76+
self, document: Document, complete_event
77+
) -> Iterable[Completion]:
78+
word = document.get_word_before_cursor()
79+
for cmd in self.COMMANDS:
80+
if cmd.startswith(word):
81+
yield Completion(cmd, -len(word))
82+
83+
5784
class MathicsCompleter(WordCompleter):
5885
def __init__(self, definitions):
5986
self.definitions = definitions

mathicsscript/interrupt.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""
2+
mathicsscript Interrupt routines.
3+
4+
Note: other environments may build on or use other interrupt handlers
5+
"""
6+
7+
import signal
8+
import subprocess
9+
import sys
10+
from types import FrameType
11+
from typing import Callable, Optional
12+
13+
from numpy import true_divide
14+
15+
from mathics import settings
16+
from mathics.core.evaluation import Evaluation
17+
from mathics.core.interrupt import AbortInterrupt, ReturnInterrupt, TimeoutInterrupt
18+
from mathics.eval.stackframe import find_Mathics3_evaluation_method, get_eval_Expression
19+
20+
21+
# See also __main__'s interactive_eval_loop
22+
def inspect_eval_loop(evaluation: Evaluation):
23+
"""
24+
A read eval/loop for an Interrupt's "inspect" command.
25+
"""
26+
shell = evaluation.shell
27+
if shell is not None:
28+
was_inside_interrupt = shell.is_inside_interrupt
29+
shell.is_inside_interrupt = True
30+
else:
31+
was_inside_interrupt = False
32+
33+
previous_recursion_depth = evaluation.recursion_depth
34+
while True:
35+
try:
36+
# Reset line number within an In[] line number.
37+
# Note: this is not setting as, say, In[5]
38+
# to back to In[1], but instead it sets the line number position *within*
39+
# In[5]. The user input for "In[5]" might have several continuation lines.
40+
if shell is not None and hasattr(shell, "lineno"):
41+
shell.lineno = 0
42+
43+
query, source_code = evaluation.parse_feeder_returning_code(shell)
44+
# show_echo(source_code, evaluation)
45+
if len(source_code) and source_code[0] == "!" and shell is not None:
46+
subprocess.run(source_code[1:], shell=True)
47+
if shell.definitions is not None:
48+
shell.definitions.increment_line_no(1)
49+
continue
50+
if query is None:
51+
continue
52+
result = evaluation.evaluate(query, timeout=settings.TIMEOUT)
53+
if result is not None and shell is not None:
54+
shell.print_result(result, prompt=False, strict_wl_output=True)
55+
except TimeoutInterrupt:
56+
print("\nTimeout occurred - ignored.")
57+
pass
58+
except ReturnInterrupt:
59+
evaluation.last_eval = None
60+
evaluation.exc_result = None
61+
evaluation.message("Interrupt", "dgend")
62+
raise
63+
except KeyboardInterrupt:
64+
print("\nKeyboardInterrupt")
65+
except EOFError:
66+
print()
67+
raise
68+
except SystemExit:
69+
# raise to pass the error code on, e.g. Quit[1]
70+
raise
71+
finally:
72+
evaluation.recursion_depth = previous_recursion_depth
73+
if shell is not None:
74+
shell.is_inside_interrupt = was_inside_interrupt
75+
76+
77+
def Mathics3_interrupt_handler(
78+
evaluation: Optional[Evaluation], interrupted_frame: FrameType, print_fn: Callable
79+
):
80+
81+
shell = evaluation.shell
82+
incolors = shell.incolors
83+
if hasattr(shell, "bottom_toolbar"):
84+
from mathicsscript.completion import InterruptCompleter
85+
86+
is_prompt_toolkit = True
87+
use_HTML = True
88+
completer = InterruptCompleter()
89+
else:
90+
is_prompt_toolkit = False
91+
use_HTML = False
92+
completer = None
93+
while True:
94+
try:
95+
prompt = (
96+
"<b>interrupt> </b>"
97+
if is_prompt_toolkit
98+
else f"{incolors[0]}interrupt> {incolors[3]}"
99+
)
100+
user_input = shell.read_line(prompt, completer, use_HTML).strip()
101+
if user_input in ("a", "abort"):
102+
print_fn("aborting")
103+
raise AbortInterrupt
104+
elif user_input in ("continue", "c"):
105+
print_fn("continuing")
106+
break
107+
elif user_input in ("exit", "quit"):
108+
print_fn("Mathics3 exited because of an interrupt.")
109+
sys.exit(3)
110+
elif user_input in ("inspect", "i"):
111+
print_fn("inspecting")
112+
if evaluation is not None:
113+
evaluation.message("Interrupt", "dgbgn")
114+
inspect_eval_loop(evaluation)
115+
116+
elif user_input in ("show", "s"):
117+
# In some cases we can better, by going back to the caller
118+
# and reconstructing the actual call with arguments.
119+
eval_frame = find_Mathics3_evaluation_method(interrupted_frame)
120+
if eval_frame is None:
121+
continue
122+
eval_method_name = eval_frame.f_code.co_name
123+
eval_method = getattr(eval_frame.f_locals.get("self"), eval_method_name)
124+
if eval_method:
125+
print_fn(eval_method.__doc__)
126+
eval_expression = get_eval_Expression()
127+
if eval_expression is not None:
128+
print_fn(shell.fmt_fn(eval_expression))
129+
break
130+
elif user_input in ("trace", "t"):
131+
print_fn("tracing")
132+
else:
133+
print_fn(
134+
"""Your options are:
135+
abort (or a) to abort current calculation
136+
continue (or c) to continue
137+
exit (or quit) to exit Mathics3
138+
inspect (or i) to enter an interactive dialog
139+
show (or s) to show current operation (and then continue)
140+
"""
141+
)
142+
except KeyboardInterrupt:
143+
print_fn("\nKeyboardInterrupt")
144+
except EOFError:
145+
print_fn("")
146+
break
147+
except TimeoutInterrupt:
148+
# There might have been a Pause[] set before we entered
149+
# this handler. If that happens, we can clear the
150+
# error. Ideally the interrupt REPL would would have clear
151+
# all timeout signals, but Python doesn't support that, as
152+
# far as I know.
153+
#
154+
# Here, we note we have time'd out. This also silences
155+
# other handlers that we've handled this.
156+
if evaluation is not None:
157+
evaluation.timeout = True
158+
break
159+
except ReturnInterrupt:
160+
# the interrupt shell probably isssued a Return[].
161+
# Respect that.
162+
break
163+
except RuntimeError:
164+
break
165+
finally:
166+
pass
167+
168+
169+
def Mathics3_basic_signal_handler(sig: int, interrupted_frame: Optional[FrameType]):
170+
"""
171+
Custom signal handler for SIGINT (Ctrl+C).
172+
"""
173+
evaluation: Optional[Evaluation] = None
174+
# Find an evaluation object to pass to the Mathics3 interrupt handler
175+
while interrupted_frame is not None:
176+
if (
177+
evaluation := interrupted_frame.f_locals.get("evaluation")
178+
) is not None and isinstance(evaluation, Evaluation):
179+
break
180+
interrupted_frame = interrupted_frame.f_back
181+
print_fn = evaluation.print_out if evaluation is not None else print
182+
print_fn("")
183+
if interrupted_frame is None:
184+
print("Unable to find Evaluation frame to start on")
185+
Mathics3_interrupt_handler(evaluation, interrupted_frame, print_fn)
186+
187+
188+
def Mathics3_USR1_signal_handler(sig: int, interrupted_frame: Optional[FrameType]):
189+
"""
190+
Custom signal handler for SIGUSR1. When we get this signal, try to
191+
find an Expression that is getting evaluated, and print that. Then
192+
continue.
193+
"""
194+
shell = None
195+
print_fn = print
196+
while interrupted_frame is not None:
197+
if (
198+
evaluation := interrupted_frame.f_locals.get("evaluation")
199+
) is not None and isinstance(evaluation, Evaluation):
200+
print_fn = evaluation.shell.errmsg
201+
break
202+
interrupted_frame = interrupted_frame.f_back
203+
204+
print_fn(f"USR1 ({sig}) interrupt")
205+
if (eval_expression := get_eval_Expression()) is not None:
206+
# If eval string is long, take just the first 100 characters
207+
# of it.
208+
if shell is not None:
209+
eval_expression_str = shell.fmt_fn(eval_expression)
210+
else:
211+
eval_expression_str = str(eval_expression)
212+
213+
if len(eval_expression_str) > 100:
214+
eval_expression_str = eval_expression_str[:100] + "..."
215+
216+
print(f"Expression: {eval_expression_str}")
217+
218+
219+
def setup_signal_handler():
220+
signal.signal(signal.SIGINT, Mathics3_basic_signal_handler)
221+
signal.signal(signal.SIGUSR1, Mathics3_USR1_signal_handler)

0 commit comments

Comments
 (0)