Repository: Textualize/rich

Issue:
duplicated plots in jupyter
How can we avoid having duplicated plots on jupyter?

When I run

```
import gdsfactory as gf
gf.components.bend_circular()
```

I get one plot in jupyter, thanks to  defining `def _repr_html_(self)`

However when I run 
```
from rich import pretty

pretty.install()
gf.components.bend_circular()
```

I get two plots of the same component.



Discussion:
Thank you for your issue. Give us a little time to review it.

PS. You might want to check the [FAQ](https://github.com/textualize/rich/blob/master/FAQ.md) if you haven't done so already.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
What version of Jupyter?
I have seen it on 3.5.2  and also on  3.6.1

Also I can see it on the docs of the package where we modify the `_repr_html_(self)`
https://gdsfactory.github.io/gdsfactory/notebooks/03_cells_autoname_and_cache.html#

```
A high level console interface.                                            │
│                                                                            │
│ ╭────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=115 ColorSystem.TRUECOLOR>                              │ │
│ ╰────────────────────────────────────────────────────────────────────────╯ │
│                                                                            │
│     color_system = 'truecolor'                                             │
│         encoding = 'utf-8'                                                 │
│             file = <ipykernel.iostream.OutStream object at 0x7efcfe5e6050> │
│           height = 100                                                     │
│    is_alt_screen = False                                                   │
│ is_dumb_terminal = False                                                   │
│   is_interactive = False                                                   │
│       is_jupyter = True                                                    │
│      is_terminal = False                                                   │
│   legacy_windows = False                                                   │
│         no_color = False                                                   │
│          options = ConsoleOptions(                                         │
│                        size=ConsoleDimensions(width=115, height=100),      │
│                        legacy_windows=False,                               │
│                        min_width=1,                                        │
│                        max_width=115,                                      │
│                        is_terminal=False,                                  │
│                        encoding='utf-8',                                   │
│                        max_height=100,                                     │
│                        justify=None,                                       │
│                        overflow=None,                                      │
│                        no_wrap=False,                                      │
│                        highlight=None,                                     │
│                        markup=None,                                        │
│                        height=None                                         │
│                    )                                                       │
│            quiet = False                                                   │
│           record = False                                                   │
│         safe_box = True                                                    │
│             size = ConsoleDimensions(width=115, height=100)                │
│        soft_wrap = False                                                   │
│           stderr = False                                                   │
│            style = None                                                    │
│         tab_size = 8                                                       │
│            width = 115                                                     │
╰────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'xterm-color',         │
│     'COLORTERM': 'truecolor',      │
│     'CLICOLOR': '1',               │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': 'tmux',        │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JUPYTER_COLUMNS': None,       │
│     'JUPYTER_LINES': None,         │
│     'JPY_PARENT_PID': '715767',    │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Linux"
```


Jupyter lab, Jupyter notebook, VScode ?
jupyter-lab as well as VSCode
I think this is a bug in the `gdsfactory` library; they use side-effects in `_repr_html_` rather than returning HTML. As such, rich probably invokes subsequent display methods, which leads to double plotting. 
Specifically https://github.com/gdsfactory/gdsfactory/blob/b1d857446e1918b6f3db5b1cce3ba4de165652a5/gdsfactory/component.py#L1253 which leads to https://github.com/gdsfactory/gdsfactory/blob/b1d857446e1918b6f3db5b1cce3ba4de165652a5/gdsfactory/component.py#L1271.

If multiple reprs are needed then `__repr_mimebundle__` may be a better approach? 
If they want to keep the side-effect pattern, [`_ipython_display_()`](https://ipython.readthedocs.io/en/stable/config/integrating.html#MyObject._ipython_display_) is the method for that. That would be the shortest path to a solution, I think.

As @blois mentioned, the direct cause is the side effects in `_repr_html_`. But since the `_ipy_display_hook` _calls_ all possible IPython hooks, IPython's own display formatter also calls the same hook, triggering the side effect a second time.

If `_repr_html_` were fixed to follow the spec and _return_ the html representation (note: plotting matplotlib figures does _not_ usually produce HTML, it produces a png or jpeg output), the duplication would be solved, but the whole plot would still be rendered twice. One would just be discarded.

I think rich's IPython hook as implemented now doesn't _quite_ fit the design of IPython's display formatters as it's implemented today. There are two classes:

1. the [DisplayFormatter](https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.formatters.html#IPython.core.formatters.DisplayFormatter), which is the object responsible for selecting and dispatching renderers for individual formatters (available as `get_ipython().display_formatter`), and
2. the individual subclasses of [BaseFormatter](https://ipython.readthedocs.io/en/stable/api/generated/IPython.core.formatters.html#IPython.core.formatters.BaseFormatter), which are registered by mime-type as `ip.display_formatter.formatters` and should _return_ representations without side effects.

Currently, `_ipy_display_hook` registers itself as `text/plain` but actually relies on side effects to display text. I'm assuming this is why other formats are  considered - if rich and the HTMLFormatter both produce output, you'll see it twice. But this isn't how plain text reprs are expected to behave. Rich is reimplementing logic in DisplayFormatter and the frontend (output format _selection_) at the level of a single formatter (text/plain), which (in IPython's opinion) should not concern itself with what other formats are computed.

IPython and Jupyter generally expect _multiple_ representations of outputs, and `text/plain` is ~always present as a fallback. I think the right fix is for rich's `_ipy_display_hook` to remove its checks for whether it should run (which don't work in general because formatter functions can also be registered without special method names using `formatter.for_type`) and compute and _return_ its text representation unconditionally, [as is done in IPython's default pretty printer](https://github.com/ipython/ipython/blob/9663a9adc4c87490f1dc59c8b6f32cdfd0c5094a/IPython/core/formatters.py#L694-L708). It's up to the DisplayFormatter to pick which representations to _compute_, and up to the frontend to pick which representation to display, and multiple frontends or nbconvert exports may pick different formats to support or prefer.

Resolving patch:
diff --git a/rich/pretty.py b/rich/pretty.py
--- a/rich/pretty.py
+++ b/rich/pretty.py
@@ -55,13 +55,6 @@
     )
 
 
-JUPYTER_CLASSES_TO_NOT_RENDER = {
-    # Matplotlib "Artists" manage their own rendering in a Jupyter notebook, and we should not try to render them too.
-    # "Typically, all [Matplotlib] visible elements in a figure are subclasses of Artist."
-    "matplotlib.artist.Artist",
-}
-
-
 def _is_attr_object(obj: Any) -> bool:
     """Check if an object was created with attrs module."""
     return _has_attrs and _attr_module.has(type(obj))
@@ -122,69 +115,40 @@ def _ipy_display_hook(
     max_string: Optional[int] = None,
     max_depth: Optional[int] = None,
     expand_all: bool = False,
-) -> None:
+) -> Union[str, None]:
     # needed here to prevent circular import:
-    from ._inspect import is_object_one_of_types
     from .console import ConsoleRenderable
 
     # always skip rich generated jupyter renderables or None values
     if _safe_isinstance(value, JupyterRenderable) or value is None:
-        return
+        return None
 
     console = console or get_console()
-    if console.is_jupyter:
-        # Delegate rendering to IPython if the object (and IPython) supports it
-        #  https://ipython.readthedocs.io/en/stable/config/integrating.html#rich-display
-        ipython_repr_methods = [
-            "_repr_html_",
-            "_repr_markdown_",
-            "_repr_json_",
-            "_repr_latex_",
-            "_repr_jpeg_",
-            "_repr_png_",
-            "_repr_svg_",
-            "_repr_mimebundle_",
-        ]
-        for repr_method in ipython_repr_methods:
-            method = getattr(value, repr_method, None)
-            if inspect.ismethod(method):
-                # Calling the method ourselves isn't ideal. The interface for the `_repr_*_` methods
-                #  specifies that if they return None, then they should not be rendered
-                #  by the notebook.
-                try:
-                    repr_result = method()
-                except Exception:
-                    continue  # If the method raises, treat it as if it doesn't exist, try any others
-                if repr_result is not None:
-                    return  # Delegate rendering to IPython
-
-        # When in a Jupyter notebook let's avoid the display of some specific classes,
-        # as they result in the rendering of useless and noisy lines such as `<Figure size 432x288 with 1 Axes>`.
-        # What does this do?
-        # --> if the class has "matplotlib.artist.Artist" in its hierarchy for example, we don't render it.
-        if is_object_one_of_types(value, JUPYTER_CLASSES_TO_NOT_RENDER):
-            return
-
-    # certain renderables should start on a new line
-    if _safe_isinstance(value, ConsoleRenderable):
-        console.line()
-
-    console.print(
-        value
-        if _safe_isinstance(value, RichRenderable)
-        else Pretty(
-            value,
-            overflow=overflow,
-            indent_guides=indent_guides,
-            max_length=max_length,
-            max_string=max_string,
-            max_depth=max_depth,
-            expand_all=expand_all,
-            margin=12,
-        ),
-        crop=crop,
-        new_line_start=True,
-    )
+
+    with console.capture() as capture:
+        # certain renderables should start on a new line
+        if _safe_isinstance(value, ConsoleRenderable):
+            console.line()
+        console.print(
+            value
+            if _safe_isinstance(value, RichRenderable)
+            else Pretty(
+                value,
+                overflow=overflow,
+                indent_guides=indent_guides,
+                max_length=max_length,
+                max_string=max_string,
+                max_depth=max_depth,
+                expand_all=expand_all,
+                margin=12,
+            ),
+            crop=crop,
+            new_line_start=True,
+            end="",
+        )
+    # strip trailing newline, not usually part of a text repr
+    # I'm not sure if this should be prevented at a lower level
+    return capture.get().rstrip("\n")
 
 
 def _safe_isinstance(


Test patch:
diff --git a/tests/test_pretty.py b/tests/test_pretty.py
--- a/tests/test_pretty.py
+++ b/tests/test_pretty.py
@@ -5,7 +5,6 @@
 from collections import UserDict, defaultdict
 from dataclasses import dataclass, field
 from typing import Any, List, NamedTuple
-from unittest.mock import patch
 
 import attr
 import pytest
@@ -78,16 +77,17 @@ def test_ipy_display_hook__multiple_special_reprs():
     console = Console(file=io.StringIO(), force_jupyter=True)
 
     class Thing:
+        def __repr__(self):
+            return "A Thing"
+
         def _repr_latex_(self):
             return None
 
         def _repr_html_(self):
             return "hello"
 
-    console.begin_capture()
-    _ipy_display_hook(Thing(), console=console)
-
-    assert console.end_capture() == ""
+    result = _ipy_display_hook(Thing(), console=console)
+    assert result == "A Thing"
 
 
 def test_ipy_display_hook__no_special_repr_methods():
@@ -97,11 +97,9 @@ class Thing:
         def __repr__(self) -> str:
             return "hello"
 
-    console.begin_capture()
-    _ipy_display_hook(Thing(), console=console)
-
-    # No IPython special repr methods, so printed by Rich
-    assert console.end_capture() == "hello\n"
+    result = _ipy_display_hook(Thing(), console=console)
+    # should be repr as-is
+    assert result == "hello"
 
 
 def test_ipy_display_hook__special_repr_raises_exception():
@@ -121,33 +119,18 @@ def _repr_latex_(self):
         def _repr_html_(self):
             return "hello"
 
-    console.begin_capture()
-    _ipy_display_hook(Thing(), console=console)
+        def __repr__(self):
+            return "therepr"
 
-    assert console.end_capture() == ""
+    result = _ipy_display_hook(Thing(), console=console)
+    assert result == "therepr"
 
 
 def test_ipy_display_hook__console_renderables_on_newline():
     console = Console(file=io.StringIO(), force_jupyter=True)
     console.begin_capture()
-    _ipy_display_hook(Text("hello"), console=console)
-    assert console.end_capture() == "\nhello\n"
-
-
-def test_ipy_display_hook__classes_to_not_render():
-    console = Console(file=io.StringIO(), force_jupyter=True)
-    console.begin_capture()
-
-    class Thing:
-        def __repr__(self) -> str:
-            return "hello"
-
-    class_fully_qualified_name = f"{__name__}.{Thing.__qualname__}"
-    with patch(
-        "rich.pretty.JUPYTER_CLASSES_TO_NOT_RENDER", {class_fully_qualified_name}
-    ):
-        _ipy_display_hook(Thing(), console=console)
-    assert console.end_capture() == ""
+    result = _ipy_display_hook(Text("hello"), console=console)
+    assert result == "\nhello"
 
 
 def test_pretty():