unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter
@ 2023-04-11 23:18 James Thomas
  2023-04-12  6:44 ` Augusto Stoffel
  0 siblings, 1 reply; 6+ messages in thread
From: James Thomas @ 2023-04-11 23:18 UTC (permalink / raw)
  To: 62783; +Cc: arstoffel

[-- Attachment #1: Type: text/plain, Size: 766 bytes --]

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


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-Matplotlib-support-in-the-standard-interpreter.patch --]
[-- Type: text/patch, Size: 5324 bytes --]

From 1da8d5f35fb9009af3bded259f47e3ae07b26fd9 Mon Sep 17 00:00:00 2001
From: James Thomas <jimjoe@gmx.net>
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


[-- Attachment #3: Type: text/plain, Size: 138 bytes --]


(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)

--

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter
  2023-04-11 23:18 bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter James Thomas
@ 2023-04-12  6:44 ` Augusto Stoffel
  2023-04-12 10:32   ` James Thomas
  0 siblings, 1 reply; 6+ messages in thread
From: Augusto Stoffel @ 2023-04-12  6:44 UTC (permalink / raw)
  To: James Thomas; +Cc: 62783

On Wed, 12 Apr 2023 at 04:48, James Thomas wrote:

> 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.


Thanks, I will have a look at this soon.





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter
  2023-04-12  6:44 ` Augusto Stoffel
@ 2023-04-12 10:32   ` James Thomas
  2023-04-16 10:55     ` Augusto Stoffel
  0 siblings, 1 reply; 6+ messages in thread
From: James Thomas @ 2023-04-12 10:32 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 62783

[-- Attachment #1: Type: text/plain, Size: 232 bytes --]

Augusto Stoffel wrote:

> Thanks, I will have a look at this soon.

Please prefer this slightly modified patch. It makes it possible for
users to switch backends at the beginning with matplotlib.use even if
comint-mime is enabled.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-Matplotlib-support-in-the-standard-interpreter.patch --]
[-- Type: text/x-diff, Size: 5397 bytes --]

From 502ca4a679083a2a5334ed46a1a629001a43a0f2 Mon Sep 17 00:00:00 2001
From: James Thomas <jimjoe@gmx.net>
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 | 102 +++++++++++++++++++++++++++++++++++++------------
 1 file changed, 78 insertions(+), 24 deletions(-)

diff --git a/comint-mime.py b/comint-mime.py
index d55698c..7739671 100644
--- a/comint-mime.py
+++ b/comint-mime.py
@@ -1,12 +1,15 @@
 # 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
+        import importlib.util
+        if not importlib.util.find_spec("matplotlib"):
+            print("`comint-mime' error: IPython or Matplotlib is required")
+            return

     from base64 import encodebytes
     from functools import partial
@@ -21,19 +24,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 +38,78 @@ 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 os import environ
+
+        environ["MPLBACKEND"] = "module://emacscomintmime"
+
+        from matplotlib import _api
+        from matplotlib.backend_bases import FigureManagerBase
+        from matplotlib.backends.backend_agg import FigureCanvasAgg
+
+        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())
+        print("`comint-mime' enabled for Matplotlib")
--
2.34.1


[-- Attachment #3: Type: text/plain, Size: 4 bytes --]


--

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter
  2023-04-12 10:32   ` James Thomas
@ 2023-04-16 10:55     ` Augusto Stoffel
  2023-04-20  5:17       ` James Thomas
  0 siblings, 1 reply; 6+ messages in thread
From: Augusto Stoffel @ 2023-04-16 10:55 UTC (permalink / raw)
  To: James Thomas; +Cc: 62783

On Wed, 12 Apr 2023 at 16:02, James Thomas wrote:

> Augusto Stoffel wrote:
>
>> Thanks, I will have a look at this soon.
>
> Please prefer this slightly modified patch. It makes it possible for
> users to switch backends at the beginning with matplotlib.use even if
> comint-mime is enabled.

Okay, I tested and it works for me, but I have a question: If I call
just call the plot function I get e.g

  >>> plt.plot([1,2,1])
  [<matplotlib.lines.Line2D object at 0x7ffb2cca0a10>]

I still need to call plt.show() afterwards to see the image.  This is
better than nothing, but would be nice if the extra step wasn't
necessary.  Also, it seems I also need to call plt.close() manually,
otherwise subsequent calls to plt.plot add to the figure instead of
starting a new one.

Do you know if the mechanics here can be refined?





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter
  2023-04-16 10:55     ` Augusto Stoffel
@ 2023-04-20  5:17       ` James Thomas
  2023-04-21 12:43         ` Augusto Stoffel
  0 siblings, 1 reply; 6+ messages in thread
From: James Thomas @ 2023-04-20  5:17 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 62783

Augusto Stoffel wrote:

> Also, it seems I also need to call plt.close() manually, otherwise
> subsequent calls to plt.plot add to the figure instead of starting a
> new one.

pyplot.show displays _all_ open 'figure's. If you want to display a
particular figure, fig.show() can be used. Also, pyplot.close can close
figures selectively.

Subsequent calls of pyplot.plot add to the 'current' figure which it
keeps track of (and that can be switched).

>   >>> plt.plot([1,2,1])
>   [<matplotlib.lines.Line2D object at 0x7ffb2cca0a10>]
>
> I still need to call plt.show() afterwards to see the image. This is
> better than nothing, but would be nice if the extra step wasn't
> necessary.

I guess it would be straightforward to implement a pretty-printer for
the Line2D object that would call its figure.show, but for the sake of
completeness this would have to be done also for every kind of element
in a figure other than lines - like axes or legends.

Also, personally, I would find it annoying if every change I make to a
figure would result in a redisplay. Sometimes I want to accumulate
changes before showing the result.

(I'm rather new to all this myself and the above is only to the best of
my knowledge)

--





^ permalink raw reply	[flat|nested] 6+ messages in thread

* bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter
  2023-04-20  5:17       ` James Thomas
@ 2023-04-21 12:43         ` Augusto Stoffel
  0 siblings, 0 replies; 6+ messages in thread
From: Augusto Stoffel @ 2023-04-21 12:43 UTC (permalink / raw)
  To: James Thomas; +Cc: 62783

All right, thanks for the info.  I've installed your patch (with some
refactoring in a subsequent commit).  Let me know if all is good.  I
haven't made a new release yet, but will do so soon.

On Thu, 20 Apr 2023 at 10:47, James Thomas wrote:

> Also, personally, I would find it annoying if every change I make to a
> figure would result in a redisplay. Sometimes I want to accumulate
> changes before showing the result.
>
> (I'm rather new to all this myself and the above is only to the best of
> my knowledge)

Okay.  It seems to me that IPython's approach makes sense, whatever the
exact rules are.  But we can always refine things later.





^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2023-04-21 12:43 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-04-11 23:18 bug#62783: [PATCH] comint-mime: Add Matplotlib support in the standard interpreter James Thomas
2023-04-12  6:44 ` Augusto Stoffel
2023-04-12 10:32   ` James Thomas
2023-04-16 10:55     ` Augusto Stoffel
2023-04-20  5:17       ` James Thomas
2023-04-21 12:43         ` Augusto Stoffel

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).