Repository: Textualize/rich

Issue:
Fixed force_terminal on Jupyter
## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [ ] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [ ] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

Please describe your changes here. If this fixes a bug, please link to the issue, if possible.

Fixes behavior after running `rich.pretty.install()` Jupyter Lab:

- Do not show `<Figure size 432x288 with 1 Axes>` on Matplotlib figures
- With `force_terminal=True`. and `force_jupyter=None` (or `True`), pandas dataframe must still be rendered once (i.e., the html repr).

Screenshot below (from this [notebook](https://gist.github.com/verdimrc/221f602a90b6c0733cb29c8d665ad674)) compares the four behaviors: (1) vanilla Python, (2 and 3) rich, and (4) this PR.

![screenshot](https://user-images.githubusercontent.com/2340781/154830727-6ac50554-4cfa-4281-83cb-02ad3f6e52db.png)


Investigate this PR
I think there is some fundamental misunderstanding in this PR. We should try and understand what the user is expected, and what if anything Rich could be doing better.

https://github.com/Textualize/rich/pull/1918


Discussion:
Why would you `force_terminal` in Jupyter lab?

Please don't ask me to run something in Jupyter when you can paste a screenshot.
Thank you for taking a look. Added a screenshot to the PR description, with markers on the behaviors that this PR tries to fix.
> Why would you force_terminal in Jupyter lab?

# [Codecov](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#1918](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (9cf8499) into [master](https://codecov.io/gh/Textualize/rich/commit/e839bfb3593b0de1dca4221c32a8098e72639893?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (e839bfb) will **decrease** coverage by `0.02%`.
> The diff coverage is `98.49%`.

[![Impacted file tree graph](https://codecov.io/gh/Textualize/rich/pull/1918/graphs/tree.svg?width=650&height=150&src=pr&token=gpwvEeKjff&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master    #1918      +/-   ##
==========================================
- Coverage   99.82%   99.80%   -0.03%     
==========================================
  Files          71       71              
  Lines        6943     7057     +114     
==========================================
+ Hits         6931     7043     +112     
- Misses         12       14       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.80% <98.49%> (-0.03%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [rich/default\_styles.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9kZWZhdWx0X3N0eWxlcy5weQ==) | `100.00% <ø> (ø)` | |
| [rich/markdown.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9tYXJrZG93bi5weQ==) | `100.00% <ø> (ø)` | |
| [rich/pretty.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9wcmV0dHkucHk=) | `99.43% <97.61%> (-0.57%)` | :arrow_down: |
| [rich/syntax.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9zeW50YXgucHk=) | `99.27% <97.72%> (-0.34%)` | :arrow_down: |
| [rich/\_\_main\_\_.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9fX21haW5fXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [rich/\_inspect.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9faW5zcGVjdC5weQ==) | `100.00% <100.00%> (ø)` | |
| [rich/color.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9jb2xvci5weQ==) | `100.00% <100.00%> (ø)` | |
| [rich/console.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9jb25zb2xlLnB5) | `100.00% <100.00%> (ø)` | |
| [rich/progress.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9wcm9ncmVzcy5weQ==) | `98.43% <100.00%> (+0.31%)` | :arrow_up: |
| [rich/segment.py](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cmljaC9zZWdtZW50LnB5) | `99.34% <100.00%> (+0.05%)` | :arrow_up: |
| ... and [9 more](https://codecov.io/gh/Textualize/rich/pull/1918/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [880693d...9cf8499](https://codecov.io/gh/Textualize/rich/pull/1918?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

I'm going to need an answer to my question, and some code I can cut and paste (or a notebook).
Hi, I thought the quoted question was posted accidentally, my apology.

The motivation to let Jupyter display cell outputs as colored ASCII rather than HTML are:

1. to use the same font as the code cells. Otherwise, HTML output will be displayed with a noticeably different font.
2. to automatically wrap long ASCII texts (which is how Jupyter behaves without rich). Otherwise, long HTML output will need horizontal scrolling, which makes rich notebook to behave differently than stock Jupyter.

Below are the Python fragments to demonstrate the effect of this PR. You'll need to `pip install pandas` to run this example in Jupyter.

This fragment shows the effect on Jupyter "display_data".

```python
import pandas as pd
import rich
import rich.pretty

# Stuffs to display
d = {"A" * 200, "B" * 200}
df = pd.DataFrame({"a": range(3), "b": range(3)})

# Render "display_data" as colored text.
rich.reconfigure(force_terminal=True)
rich.pretty.install()
display(d, df, df["a"])

# Jupyter may surpress certain "display_data". For example, the semicolon makes Jupyter hide the
# "<AxesSubplot:...>" message.
df.plot();
```

Here's the side-by-side comparison:

![patched](https://user-images.githubusercontent.com/2340781/166721697-321e3003-828c-4be1-8f92-d0a95b00013e.png)

One thing to also note is this PR changes only the ipython display hook, and makes no attempt to change the Console behavior. In the next example, the Jupyter "stream_output" still defaults to HTML, and will have to be specifically instructed for colored ASCII. 

```python
# By default, render "stream_output" (e.g., stdout) as HTML.
rich.get_console().out("a", d)

# However, "stream_output" can be force rendered as colored text.
rich.console.Console(force_terminal=True, force_jupyter=False).out("a", d)
```


Resolving patch:
diff --git a/rich/_inspect.py b/rich/_inspect.py
--- a/rich/_inspect.py
+++ b/rich/_inspect.py
@@ -2,7 +2,7 @@
 
 import inspect
 from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature
-from typing import Any, Iterable, Optional, Tuple
+from typing import Any, Collection, Iterable, Optional, Tuple, Type, Union
 
 from .console import Group, RenderableType
 from .control import escape_control_codes
@@ -233,3 +233,38 @@ def _get_formatted_doc(self, object_: Any) -> Optional[str]:
         if not self.help:
             docs = _first_paragraph(docs)
         return escape_control_codes(docs)
+
+
+def get_object_types_mro(obj: Union[object, Type[Any]]) -> Tuple[type, ...]:
+    """Returns the MRO of an object's class, or of the object itself if it's a class."""
+    if not hasattr(obj, "__mro__"):
+        # N.B. we cannot use `if type(obj) is type` here because it doesn't work with
+        # some types of classes, such as the ones that use abc.ABCMeta.
+        obj = type(obj)
+    return getattr(obj, "__mro__", ())
+
+
+def get_object_types_mro_as_strings(obj: object) -> Collection[str]:
+    """
+    Returns the MRO of an object's class as full qualified names, or of the object itself if it's a class.
+
+    Examples:
+        `object_types_mro_as_strings(JSONDecoder)` will return `['json.decoder.JSONDecoder', 'builtins.object']`
+    """
+    return [
+        f'{getattr(type_, "__module__", "")}.{getattr(type_, "__qualname__", "")}'
+        for type_ in get_object_types_mro(obj)
+    ]
+
+
+def is_object_one_of_types(
+    obj: object, fully_qualified_types_names: Collection[str]
+) -> bool:
+    """
+    Returns `True` if the given object's class (or the object itself, if it's a class) has one of the
+    fully qualified names in its MRO.
+    """
+    for type_name in get_object_types_mro_as_strings(obj):
+        if type_name in fully_qualified_types_names:
+            return True
+    return False
diff --git a/rich/jupyter.py b/rich/jupyter.py
--- a/rich/jupyter.py
+++ b/rich/jupyter.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, Iterable, List, TYPE_CHECKING, Sequence
+from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence
 
 if TYPE_CHECKING:
     from rich.console import ConsoleRenderable
diff --git a/rich/pretty.py b/rich/pretty.py
--- a/rich/pretty.py
+++ b/rich/pretty.py
@@ -55,6 +55,13 @@
     )
 
 
+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))
@@ -115,7 +122,9 @@ def _ipy_display_hook(
     max_string: Optional[int] = None,
     expand_all: bool = False,
 ) -> None:
-    from .console import ConsoleRenderable  # needed here to prevent circular import
+    # 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:
@@ -148,6 +157,13 @@ def _ipy_display_hook(
                 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()


Test patch:
diff --git a/tests/test_inspect.py b/tests/test_inspect.py
--- a/tests/test_inspect.py
+++ b/tests/test_inspect.py
@@ -1,10 +1,16 @@
 import io
 import sys
 from types import ModuleType
+from typing import Sequence, Type
 
 import pytest
 
 from rich import inspect
+from rich._inspect import (
+    get_object_types_mro,
+    get_object_types_mro_as_strings,
+    is_object_one_of_types,
+)
 from rich.console import Console
 
 skip_py36 = pytest.mark.skipif(
@@ -74,6 +80,10 @@ def __dir__(self):
         return ["__init__", "broken", "method"]
 
 
+class FooSubclass(Foo):
+    pass
+
+
 @skip_py36
 def test_render():
     console = Console(width=100, file=io.StringIO(), legacy_windows=False)
@@ -350,3 +360,62 @@ class Thing:
         expected_replacement
     )
     assert render(Something, methods=True) == expected
+
+
+@pytest.mark.parametrize(
+    "obj,expected_result",
+    (
+        [object, (object,)],
+        [object(), (object,)],
+        ["hi", (str, object)],
+        [str, (str, object)],
+        [Foo(1), (Foo, object)],
+        [Foo, (Foo, object)],
+        [FooSubclass(1), (FooSubclass, Foo, object)],
+        [FooSubclass, (FooSubclass, Foo, object)],
+    ),
+)
+def test_object_types_mro(obj: object, expected_result: Sequence[Type]):
+    assert get_object_types_mro(obj) == expected_result
+
+
+@pytest.mark.parametrize(
+    "obj,expected_result",
+    (
+        # fmt: off
+        ["hi", ["builtins.str", "builtins.object"]],
+        [str, ["builtins.str", "builtins.object"]],
+        [Foo(1), [f"{__name__}.Foo", "builtins.object"]],
+        [Foo, [f"{__name__}.Foo", "builtins.object"]],
+        [FooSubclass(1), [f"{__name__}.FooSubclass", f"{__name__}.Foo", "builtins.object"]],
+        [FooSubclass, [f"{__name__}.FooSubclass", f"{__name__}.Foo", "builtins.object"]],
+        # fmt: on
+    ),
+)
+def test_object_types_mro_as_strings(obj: object, expected_result: Sequence[str]):
+    assert get_object_types_mro_as_strings(obj) == expected_result
+
+
+@pytest.mark.parametrize(
+    "obj,types,expected_result",
+    (
+        # fmt: off
+        ["hi", ["builtins.str"], True],
+        [str, ["builtins.str"], True],
+        ["hi", ["builtins.str", "foo"], True],
+        [str, ["builtins.str", "foo"], True],
+        [Foo(1), [f"{__name__}.Foo"], True],
+        [Foo, [f"{__name__}.Foo"], True],
+        [Foo(1), ["builtins.str", f"{__name__}.Foo"], True],
+        [Foo, ["builtins.int", f"{__name__}.Foo"], True],
+        [Foo(1), [f"{__name__}.FooSubclass"], False],
+        [Foo, [f"{__name__}.FooSubclass"], False],
+        [Foo(1), [f"{__name__}.FooSubclass", f"{__name__}.Foo"], True],
+        [Foo, [f"{__name__}.Foo", f"{__name__}.FooSubclass"], True],
+        # fmt: on
+    ),
+)
+def test_object_is_one_of_types(
+    obj: object, types: Sequence[str], expected_result: bool
+):
+    assert is_object_one_of_types(obj, types) is expected_result
diff --git a/tests/test_pretty.py b/tests/test_pretty.py
--- a/tests/test_pretty.py
+++ b/tests/test_pretty.py
@@ -5,6 +5,7 @@
 from collections import UserDict, defaultdict
 from dataclasses import dataclass, field
 from typing import List, NamedTuple
+from unittest.mock import patch
 
 import attr
 import pytest
@@ -125,6 +126,22 @@ def test_ipy_display_hook__console_renderables_on_newline():
     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() == ""
+
+
 def test_pretty():
     test = {
         "foo": [1, 2, 3, (4, 5, {6}, 7, 8, {9}), {}],