Subject: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter

From: James Thomas <[email-masked]>
Date: Wed, 12 Apr 2023 04:48:08 +0530

Severity: wishlist
Tags: notabug, patch

This adds support for Matplotlib using the standard interpreter (rather
than IPython). It was tested like this:

M-x run-python
M-x comint-mime-setup
>>> import matplotlib.pyplot as plt
>>> fig, ax = plt.subplots()
>>> ax.plot([1, 2, 3, 4], [1, 4, 2, 3])
>>> plt.show()

I haven't tested whether this breaks the existing IPython functionality
- but see no reason why it should.

In GNU Emacs 29.0.60 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.33, cairo version 1.16.0) of 2023-04-01 built on
 user-Inspiron-15-5518
Repository revision: 6419d78fa6f8a7794893da5a8a5d65f75a5a29fa
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12101003
System Description: Ubuntu 22.04.2 LTS

From 1da8d5f35fb9009af3bded259f47e3ae07b26fd9 Mon Sep 17 00:00:00 2001
From: James Thomas <[email-masked]>
Date: Tue, 11 Apr 2023 19:29:29 +0530
Subject: [PATCH] Add Matplotlib support in the standard interpreter

* comint-mime.py (__COMINT_MIME_setup): Add Matplotlib backend module
and loader.
---
 comint-mime.py | 100 +++++++++++++++++++++++++++++++++++++------------
 1 file changed, 76 insertions(+), 24 deletions(-)

diff --git a/comint-mime.py b/comint-mime.py
index d55698c..6c7425c 100644
--- a/comint-mime.py
+++ b/comint-mime.py
@@ -1,12 +1,16 @@
 # This file is part of https://github.com/astoff/comint-mime

 def __COMINT_MIME_setup(types):
+    ipython = False
     try:
         ipython = get_ipython()
         assert ipython
     except:
-        print("`comint-mime' error: IPython is required")
-        return
+        try:
+            from matplotlib import _api
+        except:
+            print("`comint-mime' error: IPython or Matplotlib is required")
+            return

     from base64 import encodebytes
     from functools import partial
@@ -21,19 +25,6 @@ def __COMINT_MIME_setup(types):

     SIZE_LIMIT = 4000

-    MIME_TYPES = {
-        "image/png": encoding_workaround,
-        "image/jpeg": encoding_workaround,
-        "text/latex": str.encode,
-        "text/html": str.encode,
-        "application/json": lambda d: to_json(d).encode(),
-    }
-
-    if types == "all":
-        types = MIME_TYPES
-    else:
-        types = types.split(";")
-
     def print_osc(type, encoder, data, meta):
         meta = meta or {}
         if encoder:
@@ -48,14 +39,75 @@ def __COMINT_MIME_setup(types):
             payload = encodebytes(data).decode()
         print(f"\033]5151;{header}\n{payload}\033\\")

-    ipython.enable_matplotlib("inline")
-    ipython.display_formatter.active_types = list(MIME_TYPES.keys())
-    for mime, encoder in MIME_TYPES.items():
-        ipython.display_formatter.formatters[mime].enabled = mime in types
-        ipython.mime_renderers[mime] = partial(print_osc, mime, encoder)
+    if ipython:
+        MIME_TYPES = {
+            "image/png": encoding_workaround,
+            "image/jpeg": encoding_workaround,
+            "text/latex": str.encode,
+            "text/html": str.encode,
+            "application/json": lambda d: to_json(d).encode(),
+        }

-    if types:
-        print("`comint-mime' enabled for",
-              ", ".join(t for t in types if t in MIME_TYPES.keys()))
+        if types == "all":
+            types = MIME_TYPES
+        else:
+            types = types.split(";")
+
+        ipython.enable_matplotlib("inline")
+        ipython.display_formatter.active_types = list(MIME_TYPES.keys())
+        for mime, encoder in MIME_TYPES.items():
+            ipython.display_formatter.formatters[mime].enabled = mime in types
+            ipython.mime_renderers[mime] = partial(print_osc, mime, encoder)
+
+        if types:
+            print("`comint-mime' enabled for",
+                  ", ".join(t for t in types if t in MIME_TYPES.keys()))
+        else:
+            print("`comint-mime' disabled")
     else:
-        print("`comint-mime' disabled")
+        from importlib.abc import Loader, MetaPathFinder
+        from importlib.machinery import ModuleSpec
+        from sys import meta_path
+        from matplotlib.backend_bases import FigureManagerBase
+        from matplotlib.backends.backend_agg import FigureCanvasAgg
+        from matplotlib import use
+
+        class BackendModuleLoader(Loader):
+            def create_module(self, spec):
+                return None
+            def exec_module(self, module):
+                from io import BytesIO
+                from base64 import encodebytes
+                from json import dumps as to_json
+                from pathlib import Path
+
+                class FigureCanvasEmacsComintMime(FigureCanvasAgg):
+                    manager_class = _api.classproperty(lambda cls: FigureManagerEmacsComintMime)
+
+                    def __init__(self, *args, **kwargs):
+                        super().__init__(*args, **kwargs)
+
+                class FigureManagerEmacsComintMime(FigureManagerBase):
+
+                    def __init__(self, canvas, num):
+                        super().__init__(canvas, num)
+
+                    def show(self):
+                        self.canvas.figure.draw_without_rendering()
+                        buf = BytesIO()
+                        self.canvas.print_png(buf)
+                        print_osc("image/png", encoding_workaround, buf.getvalue(), None)
+
+                module.FigureCanvas = FigureCanvasEmacsComintMime
+                module.FigureManager = FigureManagerEmacsComintMime
+
+        class BackendModuleFinder(MetaPathFinder):
+            def find_spec(self, fullname, path, target=None):
+                if fullname == 'emacscomintmime':
+                    return ModuleSpec(fullname, BackendModuleLoader())
+                else:
+                    return None
+
+        meta_path.append(BackendModuleFinder())
+        use("module://emacscomintmime")
+        print("`comint-mime' enabled for Matplotlib")
--
2.34.1

(This is a duplicate message to the maintainer; because I suspect that a
 previous direct follow-up of mine to him has gone to spam)

--