Repository: Textualize/rich (python)

Issue:
[BUG] rich.inspect on a module: AttributeError: Unknown C global variable '__class__'
**Describe the bug**

Trying to use `rich.inspect` on the module [Faiss](https://github.com/facebookresearch/faiss), (built by [these guys](https://github.com/kyamagu/faiss-wheels)) raises an `AttributeError` exception.

repro:

```shell
pip install --user faiss-cpu rich
python -c 'import rich, faiss; rich.inspect(faiss)'
```
output:

<details>
<summary>Click to expand</summary>

```python
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/__init__.py", line 168, in inspect
    _console.print(_inspect)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/console.py", line 1621, in print
    extend(render(renderable, render_options))
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/console.py", line 1259, in render
    for render_output in iter_render:
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/panel.py", line 160, in __rich_console__
    else console.measure(
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/console.py", line 1212, in measure
    measurement = Measurement.get(self, options or self.options, renderable)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 107, in get
    get_console_width(console, options)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/padding.py", line 132, in __rich_measure__
    measure_min, measure_max = Measurement.get(console, options, self.renderable)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 107, in get
    get_console_width(console, options)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/console.py", line 468, in __rich_measure__
    return measure_renderables(console, options, self.renderables)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 142, in measure_renderables
    measurements = [
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 143, in <listcomp>
    get_measurement(console, options, renderable) for renderable in renderables
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 107, in get
    get_console_width(console, options)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/table.py", line 296, in __rich_measure__
    self._calculate_column_widths(
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/table.py", line 482, in _calculate_column_widths
    width_ranges = [
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/table.py", line 483, in <listcomp>
    self._measure_column(console, options, column) for column in columns
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/table.py", line 678, in _measure_column
    _min, _max = get_render_width(console, options, cell.renderable)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 107, in get
    get_console_width(console, options)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/padding.py", line 132, in __rich_measure__
    measure_min, measure_max = Measurement.get(console, options, self.renderable)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/measure.py", line 107, in get
    get_console_width(console, options)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/pretty.py", line 270, in __rich_measure__
    pretty_str = pretty_repr(
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/pretty.py", line 753, in pretty_repr
    node = traverse(_object, max_length=max_length, max_string=max_string)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/pretty.py", line 721, in traverse
    node = _traverse(_object, root=True)
  File "/home/pbsds/.cache/pypoetry/virtualenvs/foobar-u94_3CgC-py3.9/lib/python3.9/site-packages/rich/pretty.py", line 665, in _traverse
    elif isinstance(obj, _CONTAINERS):
  File "/usr/lib/python3.9/abc.py", line 119, in __instancecheck__
    return _abc_instancecheck(cls, instance)
AttributeError: Unknown C global variable '__class__'
```
</details>

**Platform**
<details>
<summary>Click to expand</summary>

Linux - Manjaro GNOME, using lxterminal

```
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=115 ColorSystem.TRUECOLOR>                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = 'truecolor'                                                   │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 56                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=115, height=56),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=115,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=56,                                            │
│                        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=56)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 115                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
platform="Linux"
WindowsConsoleFeatures(vt=False, truecolor=False)
rich @ file:///home/pbsds/.cache/pypoetry/artifacts/1e/d2/56/58ed32503426d13d7f2181d73a291742842d3876c387b1f4ac2e3d2807/rich-10.16.2-py3-none-any.whl
```
</details>

Pull request:
Define and use a safe `isinstance` which handles exceptions
## 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.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

The "Swig global variables object" has an empty `dir()` (🤷‍♂️) so that attribute access of `.__class__` fails. I'm not sure what else we can do other than patch this particular case by checking for the `__class__` attribute.

Fixes #1838 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0de277c2..34ef0c3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Added
 
 - Fix width measurement of 353 emoji variation sequences https://github.com/Textualize/rich/pull/1832
+- Workaround for edge case of object from Faiss with no `__class__` https://github.com/Textualize/rich/issues/1838
 
 ## [11.0.0] - 2022-01-09
 
diff --git a/rich/_inspect.py b/rich/_inspect.py
index 262695b1..07b1b623 100644
--- a/rich/_inspect.py
+++ b/rich/_inspect.py
@@ -3,7 +3,7 @@ from __future__ import absolute_import
 from inspect import cleandoc, getdoc, getfile, isclass, ismodule, signature
 from typing import Any, Iterable, Optional, Tuple
 
-from .console import RenderableType, Group
+from .console import Group, RenderableType
 from .highlighter import ReprHighlighter
 from .jupyter import JupyterMixin
 from .panel import Panel
@@ -204,7 +204,8 @@ class Inspect(JupyterMixin):
                 add_row(key_text, Pretty(value, highlighter=highlighter))
         if items_table.row_count:
             yield items_table
-        else:
+        elif not_shown_count:
             yield Text.from_markup(
-                f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options."
+                f"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] "
+                f"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options."
             )
diff --git a/rich/pretty.py b/rich/pretty.py
index 57e743df..f42434ef 100644
--- a/rich/pretty.py
+++ b/rich/pretty.py
@@ -2,7 +2,6 @@ import builtins
 import dataclasses
 import inspect
 import os
-import re
 import sys
 from array import array
 from collections import Counter, UserDict, UserList, defaultdict, deque
@@ -93,7 +92,7 @@ def _ipy_display_hook(
     from .console import ConsoleRenderable  # needed here to prevent circular import
 
     # always skip rich generated jupyter renderables or None values
-    if isinstance(value, JupyterRenderable) or value is None:
+    if _safe_isinstance(value, JupyterRenderable) or value is None:
         return
 
     console = console or get_console()
@@ -124,12 +123,12 @@ def _ipy_display_hook(
                     return  # Delegate rendering to IPython
 
     # certain renderables should start on a new line
-    if isinstance(value, ConsoleRenderable):
+    if _safe_isinstance(value, ConsoleRenderable):
         console.line()
 
     console.print(
         value
-        if isinstance(value, RichRenderable)
+        if _safe_isinstance(value, RichRenderable)
         else Pretty(
             value,
             overflow=overflow,
@@ -144,6 +143,16 @@ def _ipy_display_hook(
     )
 
 
+def _safe_isinstance(
+    obj: object, class_or_tuple: Union[type, Tuple[type, ...]]
+) -> bool:
+    """isinstance can fail in rare cases, for example types with no __class__"""
+    try:
+        return isinstance(obj, class_or_tuple)
+    except Exception:
+        return False
+
+
 def install(
     console: Optional["Console"] = None,
     overflow: "OverflowMethod" = "ignore",
@@ -178,7 +187,7 @@ def install(
             builtins._ = None  # type: ignore
             console.print(
                 value
-                if isinstance(value, RichRenderable)
+                if _safe_isinstance(value, RichRenderable)
                 else Pretty(
                     value,
                     overflow=overflow,
@@ -355,7 +364,7 @@ _MAPPING_CONTAINERS = (dict, os._Environ, MappingProxyType, UserDict)
 def is_expandable(obj: Any) -> bool:
     """Check if an object may be expanded by pretty print."""
     return (
-        isinstance(obj, _CONTAINERS)
+        _safe_isinstance(obj, _CONTAINERS)
         or (is_dataclass(obj))
         or (hasattr(obj, "__rich_repr__"))
         or _is_attr_object(obj)
@@ -539,7 +548,7 @@ def traverse(
         """Get repr string for an object, but catch errors."""
         if (
             max_string is not None
-            and isinstance(obj, (bytes, str))
+            and _safe_isinstance(obj, (bytes, str))
             and len(obj) > max_string
         ):
             truncated = len(obj) - max_string
@@ -565,7 +574,7 @@ def traverse(
 
         def iter_rich_args(rich_args: Any) -> Iterable[Union[Any, Tuple[str, Any]]]:
             for arg in rich_args:
-                if isinstance(arg, tuple):
+                if _safe_isinstance(arg, tuple):
                     if len(arg) == 3:
                         key, child, default = arg
                         if default == child:
@@ -622,7 +631,7 @@ def traverse(
                             last=root,
                         )
                     for last, arg in loop_last(args):
-                        if isinstance(arg, tuple):
+                        if _safe_isinstance(arg, tuple):
                             key, child = arg
                             child_node = _traverse(child, depth=depth + 1)
                             child_node.last = last
@@ -689,7 +698,7 @@ def traverse(
 
         elif (
             is_dataclass(obj)
-            and not isinstance(obj, type)
+            and not _safe_isinstance(obj, type)
             and not fake_attributes
             and (_is_dataclass_repr(obj) or py_version == (3, 6))
         ):
@@ -722,9 +731,9 @@ def traverse(
 
                 pop_visited(obj_id)
 
-        elif isinstance(obj, _CONTAINERS):
+        elif _safe_isinstance(obj, _CONTAINERS):
             for container_type in _CONTAINERS:
-                if isinstance(obj, container_type):
+                if _safe_isinstance(obj, container_type):
                     obj_type = container_type
                     break
 
@@ -752,7 +761,7 @@ def traverse(
                 num_items = len(obj)
                 last_item_index = num_items - 1
 
-                if isinstance(obj, _MAPPING_CONTAINERS):
+                if _safe_isinstance(obj, _MAPPING_CONTAINERS):
                     iter_items = iter(obj.items())
                     if max_length is not None:
                         iter_items = islice(iter_items, max_length)
@@ -777,7 +786,7 @@ def traverse(
             pop_visited(obj_id)
         else:
             node = Node(value_repr=to_repr(obj), last=root)
-        node.is_tuple = isinstance(obj, tuple)
+        node.is_tuple = _safe_isinstance(obj, tuple)
         return node
 
     node = _traverse(_object, root=True)
@@ -812,13 +821,13 @@ def pretty_repr(
         str: A possibly multi-line representation of the object.
     """
 
-    if isinstance(_object, Node):
+    if _safe_isinstance(_object, Node):
         node = _object
     else:
         node = traverse(
             _object, max_length=max_length, max_string=max_string, max_depth=max_depth
         )
-    repr_str = node.render(
+    repr_str: str = node.render(
         max_width=max_width, indent_size=indent_size, expand_all=expand_all
     )
     return repr_str


Test patch:
diff --git a/tests/test_inspect.py b/tests/test_inspect.py
index 5a9a5761..05a78b87 100644
--- a/tests/test_inspect.py
+++ b/tests/test_inspect.py
@@ -6,7 +6,6 @@ import pytest
 from rich import inspect
 from rich.console import Console
 
-
 skip_py36 = pytest.mark.skipif(
     sys.version_info.minor == 6 and sys.version_info.major == 3,
     reason="rendered differently on py3.6",
@@ -260,3 +259,18 @@ def test_broken_call_attr():
     result = render(foo, methods=True, width=100)
     print(repr(result))
     assert expected == result
+
+
+def test_inspect_swig_edge_case():
+    """Issue #1838 - Edge case with Faiss library - object with empty dir()"""
+
+    class Thing:
+        @property
+        def __class__(self):
+            raise AttributeError
+
+    thing = Thing()
+    try:
+        inspect(thing)
+    except Exception as e:
+        assert False, f"Object with no __class__ shouldn't raise {e}"