branch: externals/drepl
commit 11bb305bed73f6d91c4eb738c21797168af9367f
Author: Augusto Stoffel <[email protected]>
Commit: Augusto Stoffel <[email protected]>

    Add support for SageMath
---
 README.org       |  1 +
 drepl-ipython.el | 11 ++++---
 drepl-ipython.py | 36 ++++++++++++---------
 drepl-sage.el    | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 drepl-sage.py    | 30 ++++++++++++++++++
 5 files changed, 155 insertions(+), 20 deletions(-)

diff --git a/README.org b/README.org
index dc17b136bb..b1d7a45149 100644
--- a/README.org
+++ b/README.org
@@ -10,6 +10,7 @@ the moment it supports the following interpreters:
 - Various *SQL and NoSQL* databases: based on 
[[https://github.com/xo/usql][usql]], requires a Go
   compiler.
 - *Node.js*: uses the built-in REPL library.
+- *SageMath*, a computer algebra system.
 
 The following features are available, subject to variations across
 different REPLs (IPython supports all of them):
diff --git a/drepl-ipython.el b/drepl-ipython.el
index 046ced42dd..629c718217 100644
--- a/drepl-ipython.el
+++ b/drepl-ipython.el
@@ -45,10 +45,10 @@ This should be a plist of configuration options in flat 
\"dotted\"
 format.  For example, to make the prompt look like the classic Python
 one and use SVG output for plots, set this variable as follows:
 
-  (DRepl.ps1 \">>> \"
-   DRepl.ps2 \"... \"
-   DRepl.ps3 \"\"
-   DRepl.separate_in \"\"
+  (PythonRepl.ps1 \">>> \"
+   PythonRepl.ps2 \"... \"
+   PythonRepl.ps3 \"\"
+   PyhtonRepl.separate_in \"\"
    InlineBackend.figure_formats [\"svg\"])
 
 Type `%config' in the shell to see a listing of all available options."
@@ -67,7 +67,8 @@ Type `%config' in the shell to see a listing of all available 
options."
 (cl-defmethod drepl--command ((_ drepl-ipython))
   `(,python-interpreter "-c" "\
 from sys import stdin; \
-exec(stdin.read(int(stdin.readline())))"))
+exec(stdin.read(int(stdin.readline()))); \
+PythonRepl.run()"))
 
 (cl-defmethod drepl--init ((repl drepl-ipython))
   (cl-call-next-method repl)
diff --git a/drepl-ipython.py b/drepl-ipython.py
index d387179e10..646de642bb 100644
--- a/drepl-ipython.py
+++ b/drepl-ipython.py
@@ -43,14 +43,14 @@ def readmsg():
         if line.startswith(b"\033="):
             return json.loads(b"".join(buffer))
         if not line.startswith(b"\033+"):
-            raise DReplError("Invalid input")
+            raise ReplError("Invalid input")
 
 
-class DReplError(Exception):
+class ReplError(Exception):
     pass
 
 
-class DReplDisplayHook(DisplayHook):
+class ReplDisplayHook(DisplayHook):
     def write_output_prompt(self):
         stdout.write(self.shell.separate_out)
         if self.do_full_cache:
@@ -64,8 +64,7 @@ class DReplDisplayHook(DisplayHook):
         super().write_format_data(format_dict, md_dict)
 
 
[email protected]
-class DRepl(InteractiveShell):
+class Repl(InteractiveShell):
     ps1 = Unicode(
         "In [{}]: ",
         help="Primary input prompt, with '{}' replaced by the execution 
count.",
@@ -79,13 +78,20 @@ class DRepl(InteractiveShell):
         help="String prepended to return values displayed in the shell.",
     ).tag(config=True)
 
-    def __init__(self, config) -> None:
+    @classmethod
+    def run(cls):
+        "Read config as a single JSON line from stdin, then start the REPL."
+        config = json.loads(stdin.readline())
+        cls.instance(config).run_loop()
+
+    def __init__(self, config):
         # Default settings
         self.config.HistoryManager.enabled = False
         # User-supplied settings
+        own_config = getattr(self.config, self.__class__.__name__)
         for k, v in config.items():
             k0, dot, k1 = k.rpartition(".")
-            cfg = getattr(self.config, k0) if dot else self.config.DRepl
+            cfg = getattr(self.config, k0) if dot else own_config
             setattr(cfg, k1, v)
         super().__init__()
         self.confirm_exit = True
@@ -100,7 +106,7 @@ class DRepl(InteractiveShell):
         self.show_banner()
 
     system = InteractiveShell.system_raw
-    displayhook_class = DReplDisplayHook
+    displayhook_class = ReplDisplayHook
 
     def make_mime_renderer(self, type, encoder):
         def renderer(data, meta=None):
@@ -113,7 +119,7 @@ class DRepl(InteractiveShell):
                     f.write(data)
                 payload = "tmp" + Path(fname).as_uri()
             else:
-                payload = base64.encodebytes(data).decode()
+                payload = b64encode(data).decode()
             stdout.write(f"\033]5151;{header}\n{payload}\033\\\n")
 
         return renderer
@@ -121,7 +127,7 @@ class DRepl(InteractiveShell):
     def enable_gui(self, gui=None):
         pass
 
-    def mainloop(self):
+    def run_loop(self):
         while True:
             try:
                 self.run_once()
@@ -131,7 +137,7 @@ class DRepl(InteractiveShell):
                     "\nDo you really want to exit ([y]/n)?", "y", "n"
                 ):
                     return
-            except (DReplError, KeyboardInterrupt) as e:
+            except (ReplError, KeyboardInterrupt) as e:
                 print(str(e) or e.__class__.__name__)
 
     def run_once(self):
@@ -143,7 +149,7 @@ class DRepl(InteractiveShell):
             op = data.pop("op")
             fun = getattr(self, "drepl_{}".format(op), None)
             if fun is None:
-                raise DReplError("Invalid op: {}".format(op))
+                raise ReplError("Invalid op: {}".format(op))
             fun(**data)
             if op in ("eval", "setoptions"):
                 self.execution_count += 1
@@ -189,6 +195,6 @@ class DRepl(InteractiveShell):
             sendmsg(id=id)
 
 
-if __name__ == "__main__":
-    config = json.loads(stdin.readline())
-    DRepl.instance(config).mainloop()
[email protected]
+class PythonRepl(Repl):
+    pass
diff --git a/drepl-sage.el b/drepl-sage.el
new file mode 100644
index 0000000000..5ec03e4de8
--- /dev/null
+++ b/drepl-sage.el
@@ -0,0 +1,97 @@
+;;; drepl-sage.el --- dREPL for SageMath  -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2023-2024  Free Software Foundation, Inc.
+
+;; Author: Augusto Stoffel <[email protected]>
+;; Keywords: languages, processes
+;; URL: https://github.com/astoff/drepl
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This file defines a shell for Sage (https://www.sagemath.org/).
+
+;;; Code:
+
+(require 'comint-mime)
+(require 'drepl-ipython)
+(require 'mathjax)
+
+;;; Customization options
+
+(defgroup drepl-sage nil
+  "Sage shell implemented via dREPL."
+  :group 'drepl
+  :group 'python
+  :link '(url-link "https://github.com/astoff/drepl";))
+
+(defcustom drepl-sage-python-interpreter '("sage-python")
+  "Python interpreter used to run Sage.
+This should be a list consisting of the executable name optionally
+followed by command line arguments."
+  :type '(choice
+          (const :tag "Use system installation" ("sage-python"))
+          (const :tag "Run via Podman"
+                 ("podman" "run" "--rm" "-i" "--entrypoint" "sage/src/bin/sage"
+                  "docker.io/sagemath/sagemath" "--python"))
+          (const :tag "Run via Docker"
+                 ("docker" "run" "--rm" "-i" "--entrypoint" "sage/src/bin/sage"
+                  "sagemath/sagemath" "--python"))
+          (repeat :tag "Custom command" string)))
+
+(defcustom drepl-sage-config nil
+  "Customization options for the Sage shell.
+See `drepl-ipython-config' to learn how to adjust this variable."
+  :type 'plist)
+
+(defvar drepl-sage--start-file
+  (expand-file-name "drepl-sage.py" (file-name-directory (macroexp-file-name)))
+  "File name of the startup script.")
+
+;;;###autoload (autoload 'drepl-sage "drepl-sage" nil t)
+(drepl--define drepl-sage :display-name "Sage")
+
+(cl-defmethod drepl--command ((_ drepl-sage))
+  `(,@drepl-sage-python-interpreter "-c" "\
+from sys import stdin; \
+exec(stdin.read(int(stdin.readline()))); \
+SageRepl.run()"))
+
+(defun drepl-sage--render-html (header data)
+  (let ((start (point)))
+    (let ((shr-fill-text nil))
+      (comint-mime-render-html header data))
+    (mathjax-typeset-region start (point))))
+
+(cl-defmethod drepl--init ((repl drepl-sage))
+  (cl-call-next-method repl)
+  (drepl--adapt-comint-to-mode ".py")
+  (push '("5151" . comint-mime-osc-handler) ansi-osc-handlers)
+  (make-local-variable 'comint-mime-renderer-alist)
+  (push '("\\`text/html\\>" . drepl-sage--render-html)
+        comint-mime-renderer-alist)
+  (let ((buffer (current-buffer)))
+    (with-temp-buffer
+      (insert-file-contents drepl-ipython--start-file)
+      (goto-char (point-max))
+      (insert-file-contents drepl-sage--start-file)
+      (process-send-string buffer (format "%s\n" (buffer-size)))
+      (process-send-region buffer (point-min) (point-max))
+      (process-send-string buffer (json-serialize drepl-sage-config))
+      (process-send-string buffer "\n"))))
+
+(provide 'drepl-sage)
+
+;;; drepl-sage.el ends here
diff --git a/drepl-sage.py b/drepl-sage.py
new file mode 100644
index 0000000000..bfdfada0cc
--- /dev/null
+++ b/drepl-sage.py
@@ -0,0 +1,30 @@
+"""SageMath interface for dREPL."""
+# NOTE: This script is appended to drepl-ipython.py
+
+from sage.misc.banner import banner
+from sage.repl.configuration import SAGE_EXTENSION
+from sage.repl.rich_output import get_display_manager
+from sage.repl.rich_output.backend_ipython import BackendIPythonNotebook
+
+mime_types["image/svg+xml"] = lambda s: s.encode() if isinstance(s, str) else s
+
+
[email protected]
+class SageRepl(Repl):
+    display_text = Unicode(
+        "latex",
+        help="The text display format (plain or latex).",
+    ).tag(config=True)
+
+    def __init__(self, config):
+        self.ps1 = "sage: "
+        self.ps2 = "....: "
+        self.ps3 = self.separate_in = ""
+        self.show_banner = banner
+        super().__init__(config)
+        self.mime_size_limit = 2**20
+        self.extension_manager.load_extension(SAGE_EXTENSION)
+        dm = get_display_manager()
+        dm.switch_backend(BackendIPythonNotebook(), shell=self)
+        dm.preferences.text = "latex"
+        dm.preferences.graphics = "vector"

Reply via email to