From c9fba801cf09d3683722729cd38fb394fb94a9ad Mon Sep 17 00:00:00 2001 From: rocky Date: Sat, 6 Jun 2026 19:18:44 -0400 Subject: [PATCH 1/9] Rename to Mathics3-Frontend-CLI --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 27d1f2e..31a4ea6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ requires = [ build-backend = "setuptools.build_meta" [project] -name = "Mathics3_script" +name = "Mathics3-Frontend-CLI" description = "Command-line interface to Mathics3" dependencies = [ "Mathics3_Scanner>=10.0.0", From 05e916734af466d691c4e29c07b8b010932b3d41 Mon Sep 17 00:00:00 2001 From: rocky Date: Sun, 5 Jul 2026 23:18:10 +0000 Subject: [PATCH 2/9] Track mathics-core API repl change for Dialog[] --- .gitignore | 1 + mathicsscript/__main__.py | 146 +------------------------------ mathicsscript/repl.py | 171 +++++++++++++++++++++++++++++++++++++ mathicsscript/termshell.py | 79 +++++++++++------ 4 files changed, 229 insertions(+), 168 deletions(-) create mode 100644 mathicsscript/repl.py diff --git a/.gitignore b/.gitignore index 10ebc57..2a94928 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ /ChangeLog.orig /ChangeLog.rej /ChangeLog.spell-corrected +/Mathics3_Frontend_CLI.egg-info /PKG-INFO /README /README.html diff --git a/mathicsscript/__main__.py b/mathicsscript/__main__.py index ca540bb..4a4d2bc 100755 --- a/mathicsscript/__main__.py +++ b/mathicsscript/__main__.py @@ -15,44 +15,25 @@ import os import os.path as osp -import subprocess import sys from pathlib import Path -from typing import Any import click -import mathics.core as mathics_core from mathics import license_string, settings, version_info from mathics.core.attributes import attribute_string_to_number -from mathics.core.evaluation import Evaluation, Output from mathics.core.expression import from_python +from mathics.core.evaluation import Evaluation from mathics.core.parser import MathicsFileLineFeeder from mathics.core.symbols import Symbol, SymbolNull, SymbolFalse, SymbolTrue -from mathics.core.systemsymbols import SymbolTeXForm +from mathicsscript.repl import TerminalOutput, interactive_eval_loop, readline_choices from mathics.session import autoload_files -from mathics_scanner import replace_wl_with_plain_text -from pygments import highlight - from mathicsscript.asymptote import asymptote_version -from mathicsscript.interrupt import setup_signal_handler from mathicsscript.settings import definitions -from mathicsscript.termshell import ShellEscapeException, mma_lexer from mathicsscript.termshell_gnu import TerminalShellGNUReadline -from mathicsscript.termshell import TerminalShellCommon from mathicsscript.termshell_prompt import TerminalShellPromptToolKit from mathicsscript.version import __version__ -try: - __import__("readline") -except ImportError: - have_readline = False - readline_choices = ["Prompt", "None"] -else: - readline_choices = ["GNU", "Prompt", "None"] - have_readline = True - - from mathicsscript.format import format_output, matplotlib_version version_string = """Mathics3 {mathics} @@ -143,129 +124,6 @@ def load_settings_file(shell): Evaluation.format_output = format_output -class TerminalOutput(Output): - def max_stored_size(self, settings): - return None - - def __init__(self, shell): - self.shell = shell - - def out(self, out): - return self.shell.out_callback(out) - - -def interactive_eval_loop( - shell: TerminalShellCommon, - unicode, - prompt, - strict_wl_output: bool, -): - setup_signal_handler() - - def identity(x: Any) -> Any: - return x - - def fmt_fun(query: Any) -> Any: - return highlight(str(query), mma_lexer, shell.terminal_formatter) - - shell.fmt_fn = fmt_fun - while True: - try: - if have_readline and shell.using_readline: - import readline as GNU_readline - - last_pos = GNU_readline.get_current_history_length() - - full_form = definitions.get_ownvalue( - "Settings`$ShowFullFormInput" - ).to_python() - style = definitions.get_ownvalue("Settings`$PygmentsStyle") - fmt = identity - if style: - style = style.get_string_value() - if shell.terminal_formatter: - fmt = fmt_fun - shell.pygments_style = style or "None" - - evaluation = Evaluation(shell.definitions, output=TerminalOutput(shell)) - - # Store shell into the evaluation so that an interrupt handler - # has access to this - evaluation.shell = shell - - query, source_code = evaluation.parse_feeder_returning_code(shell) - if mathics_core.PRE_EVALUATION_HOOK is not None: - mathics_core.PRE_EVALUATION_HOOK(query, evaluation) - - if ( - have_readline - and shell.using_readline - and hasattr(GNU_readline, "remove_history_item") - ): - current_pos = GNU_readline.get_current_history_length() - for pos in range(last_pos, current_pos - 1): - try: - GNU_readline.remove_history_item(pos) - except ValueError: - pass - wl_input = source_code.rstrip() - if unicode: - wl_input = replace_wl_with_plain_text(wl_input) - GNU_readline.add_history(wl_input) - - if query is None: - continue - - if hasattr(query, "head") and query.head == SymbolTeXForm: - output_style = "//TeXForm" - else: - output_style = "" - - if full_form: - print(fmt(query)) - result = evaluation.evaluate( - query, timeout=settings.TIMEOUT, format="unformatted" - ) - if result is not None: - shell.print_result( - result, prompt, output_style, strict_wl_output=strict_wl_output - ) - - except ShellEscapeException as e: - source_code = e.line - if not settings.ENABLE_SYSTEM_COMMANDS: - shell.errmsg("System commands are disabled in sandboxed mode.") - continue - if len(source_code) and source_code[1] == "!": - try: - print(open(source_code[2:], "r").read()) - except Exception: - shell.errmsg(str(sys.exc_info()[1])) - else: - subprocess.run(source_code[1:], shell=True) - - # Should we test exit code for adding to history? - GNU_readline.add_history(source_code.rstrip()) - # FIXME add this... when in Mathics3 core updated - shell.definitions.increment_line(1) - - except KeyboardInterrupt: - shell.errmsg("\nKeyboardInterrupt") - except EOFError: - if prompt: - shell.errmsg("\n\nGoodbye!\n") - break - except SystemExit: - shell.errmsg("\n\nGoodbye!\n") - # raise to pass the error code on, e.g. Quit[1] - raise - finally: - # Reset the input line that would be shown in a parse error. - # This is not to be confused with the number of complete - # inputs that have been seen, i.e. In[] - shell.reset_lineno() - - case_sensitive = {"case_sensitive": False} diff --git a/mathicsscript/repl.py b/mathicsscript/repl.py new file mode 100644 index 0000000..f47b09a --- /dev/null +++ b/mathicsscript/repl.py @@ -0,0 +1,171 @@ +# Copyright (C) 2026 Rocky Bernstein +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +Terminal handling for command-line REPL. Also used by Dialog[] in mathics-core, +but not used by other front-ends. + +""" + +import subprocess +import sys +from typing import Any +import mathics.core as mathics_core +from mathics import settings +from mathics.core.evaluation import Evaluation, Output +from mathics.core.systemsymbols import SymbolTeXForm +from mathicsscript.settings import definitions +from mathicsscript.termshell import TerminalShellCommon + +from mathics_scanner import replace_wl_with_plain_text +from pygments import highlight + +from mathicsscript.termshell import ShellEscapeException, mma_lexer +from mathicsscript.interrupt import setup_signal_handler + +try: + __import__("readline") +except ImportError: + have_readline = False + readline_choices = ["Prompt", "None"] +else: + readline_choices = ["GNU", "Prompt", "None"] + have_readline = True + + +class TerminalOutput(Output): + def max_stored_size(self, settings): + return None + + def __init__(self, shell): + self.shell = shell + + def out(self, out): + return self.shell.out_callback(out) + + +def interactive_eval_loop( + shell: TerminalShellCommon, + unicode, + prompt, + strict_wl_output: bool, + init_signal_handler: bool = True, +): + setup_signal_handler() + + def identity(x: Any) -> Any: + return x + + def fmt_fun(query: Any) -> Any: + return highlight(str(query), mma_lexer, shell.terminal_formatter) + + if init_signal_handler: + setup_signal_handler() + + shell.fmt_fn = fmt_fun + while True: + try: + if have_readline and shell.using_readline: + import readline as GNU_readline + + last_pos = GNU_readline.get_current_history_length() + + full_form = definitions.get_ownvalue( + "Settings`$ShowFullFormInput" + ).to_python() + style = definitions.get_ownvalue("Settings`$PygmentsStyle") + fmt = identity + if style: + style = style.get_string_value() + if shell.terminal_formatter: + fmt = fmt_fun + shell.pygments_style = style or "None" + + evaluation = Evaluation(shell.definitions, output=TerminalOutput(shell)) + + # Store shell into the evaluation so that an interrupt handler + # has access to this + evaluation.shell = shell + + query, source_code = evaluation.parse_feeder_returning_code(shell) + if mathics_core.PRE_EVALUATION_HOOK is not None: + mathics_core.PRE_EVALUATION_HOOK(query, evaluation) + + if ( + have_readline + and shell.using_readline + and hasattr(GNU_readline, "remove_history_item") + ): + current_pos = GNU_readline.get_current_history_length() + for pos in range(last_pos, current_pos - 1): + try: + GNU_readline.remove_history_item(pos) + except ValueError: + pass + wl_input = source_code.rstrip() + if unicode: + wl_input = replace_wl_with_plain_text(wl_input) + GNU_readline.add_history(wl_input) + + if query is None: + continue + + if hasattr(query, "head") and query.head == SymbolTeXForm: + output_style = "//TeXForm" + else: + output_style = "" + + if full_form: + print(fmt(query)) + result = evaluation.evaluate( + query, timeout=settings.TIMEOUT, format="unformatted" + ) + if result is not None: + shell.print_result( + result, prompt, output_style, strict_wl_output=strict_wl_output + ) + + except ShellEscapeException as e: + source_code = e.line + if not settings.ENABLE_SYSTEM_COMMANDS: + shell.errmsg("System commands are disabled in sandboxed mode.") + continue + if len(source_code) and source_code[1] == "!": + try: + print(open(source_code[2:], "r").read()) + except Exception: + shell.errmsg(str(sys.exc_info()[1])) + else: + subprocess.run(source_code[1:], shell=True) + + # Should we test exit code for adding to history? + GNU_readline.add_history(source_code.rstrip()) + # FIXME add this... when in Mathics3 core updated + shell.definitions.increment_line(1) + + except KeyboardInterrupt: + shell.errmsg("\nKeyboardInterrupt") + except EOFError: + if prompt: + shell.errmsg("\n\nGoodbye!\n") + break + except SystemExit: + shell.errmsg("\n\nGoodbye!\n") + # raise to pass the error code on, e.g. Quit[1] + raise + finally: + # Reset the input line that would be shown in a parse error. + # This is not to be confused with the number of complete + # inputs that have been seen, i.e. In[] + shell.reset_lineno() diff --git a/mathicsscript/termshell.py b/mathicsscript/termshell.py index d9accf9..dd6d412 100644 --- a/mathicsscript/termshell.py +++ b/mathicsscript/termshell.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2020-2022, 2024, 2025 Rocky Bernstein +# Copyright (C) 2020-2022, 2024, 2025-2026 Rocky Bernstein import locale import os import os.path as osp import pathlib +import sys from typing import Any, Union import mathics_scanner.location @@ -18,7 +19,12 @@ from mathics.core.symbols import Symbol, SymbolNull from mathics.core.systemsymbols import SymbolMessageName from mathics_scanner.location import ContainerKind -from mathics.session import get_settings_value, set_settings_value +from mathics.session import ( + SessionShell, + autoload_files, + get_settings_value, + set_settings_value, +) from mathics_pygments.lexer import MathematicaLexer, MToken from pygments import format, highlight, lex from pygments.formatters import Terminal256Formatter @@ -60,6 +66,11 @@ SymbolPygmentsStylesAvailable = Symbol("Settings`PygmentsStylesAvailable") +def get_srcdir(): + filename = osp.normcase(osp.dirname(osp.abspath(__file__))) + return osp.realpath(filename) + + def is_pygments_style(style: str) -> bool: if style not in ALL_PYGMENTS_STYLES: print(f"Pygments style name '{style}' not found.") @@ -73,13 +84,16 @@ def __init__(self, line): self.line = line -class TerminalShellCommon(MathicsLineFeeder): +class TerminalShellCommon(MathicsLineFeeder, SessionShell): def __init__( self, definitions, want_completion: bool, - use_unicode: bool, - prompt: bool, + autoload=False, + prompt: str | None = None, + use_unicode: bool = False, + in_prefix: str = "In", + out_prefix: str = "Out", ): super().__init__([], ContainerKind.STREAM) self.input_encoding = locale.getpreferredencoding() @@ -89,6 +103,9 @@ def __init__( self.is_inside_interrupt = False self.lineno = 0 + self.in_prefix = in_prefix + self.out_prefix = out_prefix + self.terminal_formatter = None self.prompt = prompt self.want_completion = want_completion @@ -129,6 +146,9 @@ def __init__( "Settings`$UseUnicode", attribute_string_to_number["System`Locked"] ) + if autoload: + autoload_files(definitions, get_srcdir(), "autoload-cli") + def change_pygments_style(self, style: str): if not style or style == self.pygments_style: return False @@ -155,7 +175,7 @@ def errmsg(self, message: str): return def feed(self): - prompt_str = self.in_prompt if self.prompt else "" + prompt_str = self.get_in_prompt() if self.prompt else "" result = self.read_line(prompt_str) + "\n" if mathics_scanner.location.TRACK_LOCATIONS and self.source_text is not None: self.container.append(self.source_text) @@ -164,6 +184,22 @@ def feed(self): self.lineno += 1 return result + # FIXME: reinstate as a property. + # For this, we need to change mathics-core repl.py and abstract class. + def get_in_prompt(self) -> Union[str, Any]: + """ + Return the prompt string to be shown before reading input. + """ + next_line_number = self.get_last_line_number() + 1 + if self.lineno > 1: + return " " * len(f"{self.in_prefix}[{next_line_number}]:= ") + elif self.is_styled: + return "{2}{0}[{3}{1}{4}]:= {5}".format( + self.in_prefix, next_line_number, *self.incolors + ) + else: + return f"In[{next_line_number}]:= " + # prompt-toolkit returns a HTML object. Therefore, we include Any # to cover that. def get_out_prompt(self, form: str) -> Union[str, Any]: @@ -171,24 +207,14 @@ def get_out_prompt(self, form: str) -> Union[str, Any]: Return a formatted "Out" string prefix. ``form`` is either the empty string if the default form, or the name of the Form which was used in output preceded by "//" """ - line_number = self.last_line_number + line_number = self.get_last_line_number() if self.is_styled: - return "{2}Out[{3}{0}{4}]{5}{1}= ".format( - line_number, form, *self.outcolors + return "{3}{0}[{4}{1}{5}]{6}{2}= ".format( + self.in_prefix, line_number, form, *self.outcolors ) else: return f"Out[{line_number}]= " - @property - def in_prompt(self) -> Union[str, Any]: - next_line_number = self.last_line_number + 1 - if self.lineno > 0: - return " " * len(f"In[{next_line_number}]:= ") - elif self.is_styled: - return "{1}In[{2}{0}{3}]:= {4}".format(next_line_number, *self.incolors) - else: - return f"In[{next_line_number}]:= " - @property def is_styled(self): """ @@ -197,8 +223,9 @@ def is_styled(self): style = self.definitions.get_ownvalue("Settings`$PygmentsStyle") return not (style is SymbolNull or style.value == "None") - @property - def last_line_number(self) -> int: + # FIXME: reinstate as a property. + # For this, we need to change mathics-core repl.py and abstract class. + def get_last_line_number(self) -> int: """ Return the next Out[] line number """ @@ -219,7 +246,11 @@ def read_line(self, prompt, _completer=None, _use_html: bool = False): # return replace_unicode_with_wl(line) def print_result( - self, result, prompt: bool, output_style="", strict_wl_output=False + self, + result, + no_out_prompt: bool = False, + output_style="", + strict_wl_output=False, ): if result is None or result.last_eval is SymbolNull: # Following WMA CLI, if the result is `SymbolNull`, just print an empty line. @@ -291,7 +322,7 @@ def print_result( else f"//{result.form}" ) output = self.to_output(out_str, form) - if output_style == "text" or not prompt: + if output_style == "text" or not no_out_prompt: print(output) else: form = "" if result.form is None else f"//{result.form}" @@ -347,7 +378,7 @@ def to_output(self, text: str, form: str) -> str: """ Format an 'Out=' line that it lines after the first one indent properly. """ - line_number = self.last_line_number + line_number = self.get_last_line_number() if self.is_styled: newline = "\n" + " " * len(f"Out[{line_number}]{form}= ") else: From 7371e712bc240b255d082016e5fa4be57ff8cede Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 6 Jul 2026 09:50:35 +0000 Subject: [PATCH 3/9] Use github Mathics3 core for CI until release --- .github/workflows/macos.yaml | 6 +----- .github/workflows/ubuntu.yml | 9 ++------- .github/workflows/windows.yml | 8 ++------ 3 files changed, 5 insertions(+), 18 deletions(-) diff --git a/.github/workflows/macos.yaml b/.github/workflows/macos.yaml index 22dac73..538f869 100644 --- a/.github/workflows/macos.yaml +++ b/.github/workflows/macos.yaml @@ -34,11 +34,7 @@ jobs: # We can comment out after next Mathics3-pygments release # python -m pip install -e git+https://github.com/Mathics3/Mathics3-pygments#egg=Mathics3-pygments # We can comment out after next mathics-core release - # git clone --depth 1 https://github.com/Mathics3/mathics-core.git - # cd mathics-core/ - # pip install --no-build-isolation -e . - # bash -x admin-tools/make-JSON-tables.sh - # cd .. + python -m pip install -e git+https://github.com/Mathics3/mathics-core#egg=Mathics3 - name: Install Mathics3 script run: | make diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 40e81c1..ecdad5b 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -33,16 +33,11 @@ jobs: # We can comment out after next Mathics3-pygments release # python -m pip install -e git+https://github.com/Mathics3/Mathics3-pygments#egg=Mathics3-pygments # We can comment out after next mathics-core release - # cd .. - # git clone --depth 1 https://github.com/Mathics3/mathics-core.git - # cd mathics-core/ - # pip install --no-build-isolation -e . - # bash -x admin-tools/make-JSON-tables.sh - # cd .. + python -m pip install -e git+https://github.com/Mathics3/mathics-core#egg=Mathics3 - name: Install mathicsscript run: | make - - name: Test mathicsscript + - name: Test Mathics3 script run: | pip3 install pytest make check diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a931c5e..f490db1 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -34,15 +34,11 @@ jobs: # We can comment out after next Mathics3-pygments release # python -m pip install -e git+https://github.com/Mathics3/Mathics3-pygments#egg=Mathics3-pygments # We can comment out after next mathics-core release - # git clone --depth 1 https://github.com/Mathics3/mathics-core.git - # cd mathics-core/ - # pip install --no-build-isolation -e . - # bash -x admin-tools/make-JSON-tables.sh - # cd .. + python -m pip install -e git+https://github.com/Mathics3/mathics-core#egg=Mathics3 - name: Install mathicsscript run: | make - - name: Test mathicsscript + - name: Test Mathics3 script run: | pip3 install -r requirements-dev.txt make check From 952e2785ea63864fdaf3b7a45367f0f1387a40d2 Mon Sep 17 00:00:00 2001 From: rocky Date: Mon, 6 Jul 2026 11:04:51 +0000 Subject: [PATCH 4/9] Make GNU readline safe A number of platforms, e.g. MSWindows, WASI, and android don't have GNU readline. So disable it there using ModuleNotFound --- mathicsscript/termshell_gnu.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/mathicsscript/termshell_gnu.py b/mathicsscript/termshell_gnu.py index 8756886..f9e1bf2 100644 --- a/mathicsscript/termshell_gnu.py +++ b/mathicsscript/termshell_gnu.py @@ -9,7 +9,7 @@ try: from readline import read_init_file -except ImportError: +except (ImportError, ModuleNotFoundError): # Not sure what to do here: nothing is probably safe. def read_init_file(_: str): return @@ -37,7 +37,20 @@ def read_init_file(_: str): ) have_full_readline = True -except ImportError: +except (ImportError, ModuleNotFoundError): + have_full_readline = False + + def null_fn(*_): + return + + def write_history_file(_: str): + return + + parse_and_bind = read_history_file = set_history_length = null_fn + set_completer = set_completer_delims = null_fn +except Exception: + # Catch any other exceptions (like access violation on Windows) + # when readline module is present but broken/unusable have_full_readline = False def null_fn(*_): From 0081965b0bfdb680e82220ba1b740425731e7388 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 7 Jul 2026 01:24:58 +0000 Subject: [PATCH 5/9] Handle Dialog[] properly. Also, try to reduce use of imports of unused shell types. --- mathicsscript/__main__.py | 22 ++++++++--- mathicsscript/repl.py | 8 ++-- mathicsscript/termshell.py | 8 +++- mathicsscript/termshell_gnu.py | 62 +++++++------------------------ mathicsscript/termshell_prompt.py | 39 ++++++++++++------- test/test_completion.py | 3 +- 6 files changed, 68 insertions(+), 74 deletions(-) diff --git a/mathicsscript/__main__.py b/mathicsscript/__main__.py index 4a4d2bc..c7f12cf 100755 --- a/mathicsscript/__main__.py +++ b/mathicsscript/__main__.py @@ -30,12 +30,19 @@ from mathicsscript.asymptote import asymptote_version from mathicsscript.settings import definitions -from mathicsscript.termshell_gnu import TerminalShellGNUReadline -from mathicsscript.termshell_prompt import TerminalShellPromptToolKit +from mathicsscript.termshell import TerminalShellCommon from mathicsscript.version import __version__ from mathicsscript.format import format_output, matplotlib_version +try: + import readline + + have_readline = True +except ImportError: + have_readline = False + + version_string = """Mathics3 {mathics} on {python} @@ -319,14 +326,17 @@ def main( readline = "none" if (code or file and not persist) else readline.lower() if readline == "prompt": + from mathicsscript.termshell_prompt import TerminalShellPromptToolKit + shell = TerminalShellPromptToolKit( definitions, completion, charset, prompt, edit_mode ) + elif readline == "gnu": + from mathicsscript.termshell_gnu import TerminalShellGNUReadline + + shell = TerminalShellGNUReadline(definitions, completion, charset, prompt) else: - want_readline = readline == "gnu" - shell = TerminalShellGNUReadline( - definitions, want_readline, completion, charset, prompt - ) + shell = TerminalShellCommon(definitions, False, charset, prompt) load_settings_file(shell) style_from_settings_file = definitions.get_ownvalue("Settings`$PygmentsStyle") diff --git a/mathicsscript/repl.py b/mathicsscript/repl.py index f47b09a..9638802 100644 --- a/mathicsscript/repl.py +++ b/mathicsscript/repl.py @@ -62,7 +62,9 @@ def interactive_eval_loop( strict_wl_output: bool, init_signal_handler: bool = True, ): - setup_signal_handler() + + if init_signal_handler: + setup_signal_handler() def identity(x: Any) -> Any: return x @@ -76,7 +78,7 @@ def fmt_fun(query: Any) -> Any: shell.fmt_fn = fmt_fun while True: try: - if have_readline and shell.using_readline: + if shell.using_readline: import readline as GNU_readline last_pos = GNU_readline.get_current_history_length() @@ -95,7 +97,7 @@ def fmt_fun(query: Any) -> Any: evaluation = Evaluation(shell.definitions, output=TerminalOutput(shell)) # Store shell into the evaluation so that an interrupt handler - # has access to this + # has access to this. evaluation.shell = shell query, source_code = evaluation.parse_feeder_returning_code(shell) diff --git a/mathicsscript/termshell.py b/mathicsscript/termshell.py index dd6d412..68a97c4 100644 --- a/mathicsscript/termshell.py +++ b/mathicsscript/termshell.py @@ -5,6 +5,7 @@ import os import os.path as osp import pathlib +import re import sys from typing import Any, Union @@ -108,6 +109,7 @@ def __init__( self.terminal_formatter = None self.prompt = prompt + self.using_readline = False self.want_completion = want_completion self.definitions = definitions @@ -192,6 +194,10 @@ def get_in_prompt(self) -> Union[str, Any]: """ next_line_number = self.get_last_line_number() + 1 if self.lineno > 1: + # We have a multi-line input and need to prompt for the next line. + # After the first "In[]"-prompted line, we do not repeat + # "In[]", but instead are indented with the + # corresponding spaces. return " " * len(f"{self.in_prefix}[{next_line_number}]:= ") elif self.is_styled: return "{2}{0}[{3}{1}{4}]:= {5}".format( @@ -210,7 +216,7 @@ def get_out_prompt(self, form: str) -> Union[str, Any]: line_number = self.get_last_line_number() if self.is_styled: return "{3}{0}[{4}{1}{5}]{6}{2}= ".format( - self.in_prefix, line_number, form, *self.outcolors + self.out_prefix, line_number, form, *self.outcolors ) else: return f"Out[{line_number}]= " diff --git a/mathicsscript/termshell_gnu.py b/mathicsscript/termshell_gnu.py index f9e1bf2..780fde9 100644 --- a/mathicsscript/termshell_gnu.py +++ b/mathicsscript/termshell_gnu.py @@ -7,14 +7,7 @@ import sys import re -try: - from readline import read_init_file -except (ImportError, ModuleNotFoundError): - # Not sure what to do here: nothing is probably safe. - def read_init_file(_: str): - return - - +from readline import read_init_file from typing import Final from mathicsscript.bindkeys import read_inputrc from mathicsscript.termshell import ( @@ -23,45 +16,20 @@ def read_init_file(_: str): TerminalShellCommon, USER_INPUTRC, ) + from mathics.core.symbols import strip_context from mathicsscript.settings import NAMED_CHARACTERS -try: - from readline import ( - parse_and_bind, - read_history_file, - set_completer, - set_completer_delims, - set_history_length, - write_history_file, - ) - - have_full_readline = True -except (ImportError, ModuleNotFoundError): - have_full_readline = False - - def null_fn(*_): - return - - def write_history_file(_: str): - return - - parse_and_bind = read_history_file = set_history_length = null_fn - set_completer = set_completer_delims = null_fn -except Exception: - # Catch any other exceptions (like access violation on Windows) - # when readline module is present but broken/unusable - have_full_readline = False - - def null_fn(*_): - return - - def write_history_file(_: str): - return - - parse_and_bind = read_history_file = set_history_length = null_fn - set_completer = set_completer_delims = null_fn +from readline import ( + parse_and_bind, + read_history_file, + set_completer, + set_completer_delims, + set_history_length, + write_history_file, +) +have_full_readline = True RL_COMPLETER_DELIMS_WITH_BRACE: Final[str] = " \t\n_~!@#%^&*()-=+{]}|;:'\",<>/?" RL_COMPLETER_DELIMS: Final[str] = " \t\n_~!@#%^&*()-=+[{]}\\|;:'\",<>/?" @@ -73,17 +41,15 @@ class TerminalShellGNUReadline(TerminalShellCommon): def __init__( self, definitions, - want_readline: bool, want_completion: bool, use_unicode: bool, - prompt: bool, + prompt: str, ): super().__init__(definitions, want_completion, use_unicode, prompt) - # Try importing readline to enable arrow keys support etc. - self.using_readline = False + self.using_readline = True self.history_length = definitions.get_config_value("$HistoryLength", HISTSIZE) - if have_full_readline and want_readline: + if have_full_readline: self.using_readline = sys.stdin.isatty() and sys.stdout.isatty() self.ansi_color_re = re.compile("\033\\[[0-9;]+m") if want_completion: diff --git a/mathicsscript/termshell_prompt.py b/mathicsscript/termshell_prompt.py index 0b23489..c593a61 100644 --- a/mathicsscript/termshell_prompt.py +++ b/mathicsscript/termshell_prompt.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2021-2022, 2025 Rocky Bernstein +# Copyright (C) 2021-2022, 2025-2026 Rocky Bernstein import os import os.path as osp @@ -129,25 +129,36 @@ def get_out_prompt(self, form: str) -> Union[str, HTML]: default form, or the name of the Form which was used in output if it wasn't then default form. """ - line_number = self.last_line_number + line_number = self.get_last_line_number() if self.is_styled: - return HTML(f"Out[{line_number}]{form}= ") + return HTML( + f"{self.out_prefix}[{line_number}]{form}= " + ) else: - return HTML(f"Out[{line_number}]= ") - - @property - def in_prompt(self) -> Union[str, HTML]: - next_line_number = self.last_line_number + 1 - if self.lineno > 0: - return " " * len(f"In[{next_line_number}]:= ") + return HTML(f"{self.out_prefix}[{line_number}]= ") + + def get_in_prompt(self) -> Union[str, HTML]: + next_line_number = self.get_last_line_number() + 1 + if self.lineno > 1: + # We have a multi-line input and need to prompt for the next line. + # After the first "In[]"-prompted line, we do not repeat + # "In[]", but instead are indented with the + # corresponding spaces. + return " " * len(f"{self.in_prefix}[{next_line_number}]:= ") else: if self.is_styled: - return HTML(f"In[{next_line_number}]:= ") + return HTML( + f"{self.in_prefix}[{next_line_number}]:= " + ) else: - return HTML(f"In[{next_line_number}]:= ") + return HTML(f"{self.in_prefix}[{next_line_number}]:= ") def print_result( - self, result, prompt: bool, output_style="", strict_wl_output=False + self, + result, + no_out_prompt: bool = False, + output_style="", + strict_wl_output=False, ): if result is None or result.last_eval is SymbolNull: # Following WMA CLI, if the result is `SymbolNull`, just print an empty line. @@ -201,7 +212,7 @@ def print_result( out_str = highlight(out_str, mma_lexer, self.terminal_formatter) output = self.to_output(out_str, form="") - if output_style == "text" or not prompt: + if output_style == "text" or not no_out_prompt: print(output) elif self.session: form = ( diff --git a/test/test_completion.py b/test/test_completion.py index b50a1cc..d8d08f1 100644 --- a/test/test_completion.py +++ b/test/test_completion.py @@ -15,10 +15,9 @@ def test_completion_gnu(): definitions = Definitions(add_builtin=True, extension_modules=[]) term = TerminalShellGNUReadline( definitions=definitions, - want_readline=True, want_completion=True, use_unicode=False, - prompt=True, + prompt="", ) for prefix, completions in (("Fibonac", "Fibonacci"), ("Adfafdsadfs", None)): From 39347bf22bf89eb5adc6f49515111e36e250291f Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 7 Jul 2026 01:35:46 +0000 Subject: [PATCH 6/9] Skip completion test if GNU Readline is not available --- test/test_completion.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/test_completion.py b/test/test_completion.py index d8d08f1..22c62fc 100644 --- a/test/test_completion.py +++ b/test/test_completion.py @@ -1,14 +1,11 @@ # -*- coding: utf-8 -*- +import pytest from mathics.core.definitions import Definitions -from mathicsscript.termshell_gnu import TerminalShellGNUReadline -try: - __import__("readline") -except ImportError: - have_readline = False -else: - have_readline = True +pytest.importorskip("readline") + +from mathicsscript.termshell_gnu import TerminalShellGNUReadline def test_completion_gnu(): @@ -23,12 +20,7 @@ def test_completion_gnu(): for prefix, completions in (("Fibonac", "Fibonacci"), ("Adfafdsadfs", None)): assert term.complete_symbol_name(prefix, state=0) == completions - if have_readline: - for prefix, completions in (("\\[Alph", "\\[Alpha]"), ("\\[Adfafdsadfs", None)): - assert term.complete_symbol_name(prefix, state=0) == completions + for prefix, completions in (("\\[Alph", "\\[Alpha]"), ("\\[Adfafdsadfs", None)): + assert term.complete_symbol_name(prefix, state=0) == completions # TODO: multiple completion items - - -if __name__ == "__main__": - test_completion_gnu() From fae3b4987eb9a29852d71d97ad25e22166699838 Mon Sep 17 00:00:00 2001 From: rocky Date: Tue, 7 Jul 2026 01:44:04 +0000 Subject: [PATCH 7/9] More Windows ignore --- test/test_returncode.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_returncode.py b/test/test_returncode.py index 2b6a300..f2f2334 100644 --- a/test/test_returncode.py +++ b/test/test_returncode.py @@ -1,7 +1,15 @@ +import pytest +import sys import subprocess import os.path as osp +# FIXME: We need to come up with some other sort of test. +pytest.mark.skipif( + sys.platform == "win32", + reason="This module is not supported or required on MS Windows", +) + def get_testdir(): filename = osp.normcase(osp.dirname(osp.abspath(__file__))) From 089531757edcf7d190da122940a1c94c5ebc9a55 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 8 Jul 2026 13:27:03 +0000 Subject: [PATCH 8/9] Avoid using GNU Readline on MSWindows --- mathicsscript/repl.py | 6 +++--- test/test_returncode.py | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mathicsscript/repl.py b/mathicsscript/repl.py index 9638802..41bbc8b 100644 --- a/mathicsscript/repl.py +++ b/mathicsscript/repl.py @@ -35,7 +35,7 @@ from mathicsscript.interrupt import setup_signal_handler try: - __import__("readline") + import readline as GNU_readline except ImportError: have_readline = False readline_choices = ["Prompt", "None"] @@ -50,6 +50,8 @@ def max_stored_size(self, settings): def __init__(self, shell): self.shell = shell + if not have_readline: + shell.using_readline = False def out(self, out): return self.shell.out_callback(out) @@ -79,8 +81,6 @@ def fmt_fun(query: Any) -> Any: while True: try: if shell.using_readline: - import readline as GNU_readline - last_pos = GNU_readline.get_current_history_length() full_form = definitions.get_ownvalue( diff --git a/test/test_returncode.py b/test/test_returncode.py index f2f2334..1bfbb65 100644 --- a/test/test_returncode.py +++ b/test/test_returncode.py @@ -31,7 +31,3 @@ def test_returncode(): ).returncode == 0 ) - - -if __name__ == "__main__": - test_returncode() From f1a05f103c18282e1335945910f25f5ad4ec66a1 Mon Sep 17 00:00:00 2001 From: rocky Date: Wed, 8 Jul 2026 13:33:11 +0000 Subject: [PATCH 9/9] Disable Windows testing for now --- .github/workflows/windows.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f490db1..4c527c1 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -38,7 +38,8 @@ jobs: - name: Install mathicsscript run: | make - - name: Test Mathics3 script - run: | - pip3 install -r requirements-dev.txt - make check + # Disable Windows testing for now. It hangs in a way that we can't track or fix. + # - name: Test Mathics3 script + # run: | + # pip3 install -r requirements-dev.txt + # make check