← pod_r5_v1i

rich_3894

resolved RESOLVED PASS · 21 tool calls · 164 s · Textualize/rich

Task input

Handle unusual __qualname__ in inspect

<!--
Please note that Rich isn't accepting any new features at this point.

If a feature can be implemented without modifying the core library, then
they should be released as a third-party module. I can accept updates to the
core library that make it easier to extend (think hooks).

Bugfixes are always welcome of course.

Sometimes it is not clear what is a feature and what is a bug fix.
If there is any doubt, please open a discussion first.

-->

<!--
*Are you contributing typo fixes?*

If your PR solely consists of typos, at least one must be in the docs to warrant an addition to CONTRIBUTORS.md
-->

## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [x] 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 (see note about typos above).
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

I discovered, that an object with an unusual `__qualname__` attribute (not a `str`, e.g. a descriptor) causes `inspect` to fail.
This can happen if an object is created this way intentionally, but I discovered it, when decorating a class with `@functools.lru_cache` which forwards the underlying attributes using a descriptor when it can't add the to it's `__dict__` because the class uses `__slots__`:

```python
from functools import lru_cache
from rich import inspect


@lru_cache
class Klaas:
    __slots__ = ("__qualname__",)


if __name__ == "__main__":
    from rich.traceback import install

    install()

    inspect(Klaas.__qualname__)
    inspect(Klaas, all=True)
```

I fixed it by checking the type of `__qualname__` falling back to `__name__` and if that isn't a `str` using the already provided fallback.

Tool calls (22)

#ToolArgumentsResult
1run_command
{
 "command": "cat > /tmp/reproduce_issue.py <<'PY'\nfrom functools import lru_cache\nfrom rich import inspect\n\n@lru_cache\nclass Klaas:\n    __slots__ = (\"__qualname__\",)\n\nif __name__ == \"__main__\":\n    print(\"Testing inspect(Klaas.__qualname__)...\")\n    try:\n        inspect(Klaas.__qualname__)\n        print(\"Success!\")\n    except Exception as e:\n        print(f\"Failed: {e}\")\n\n    print(\"\\nTesting inspect(Klaas, all=True)...\")\n    try:\n        inspect(Klaas, all=True)\n        print(\"Success!\")\n    except Exception as e:\n        print(f\"Failed: {e}\")\nPY\npython3 /tmp/reproduce_issue.py\n"
}
{"status": "ok", "stdout": "Testing inspect(Klaas.__qualname__)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
full
{"status": "ok", "stdout": "Testing inspect(Klaas.__qualname__)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <class 'member_descriptor'> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\n\u2502 \u2502 <member '__qualname__' of 'Klaas' objects>               \u2502 \u2502\n\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u2502\n\u2502                                                              \u2502\n\u2502 30 attribute(s) not shown. Run inspect(inspect) for options. \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\nSuccess!\n\nTesting inspect(Klaas, all=True)...\nFailed: Only str or Text can be appended to Text\n", "stderr": "", "exit_code": 0}
2run_command
{
 "command": "# 2: find where 'inspect' function is defined and how it gets the name of the object.\ngrep -rn \"def inspect\" rich/ | grep \".py\"\n"
}
{"status": "ok", "stdout": "rich/__init__.py:120:def inspect(\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "rich/__init__.py:120:def inspect(\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "# 3: view rich/__init__.py around def inspect\nsed -n '120,200p' rich/__init__.py\n"
}
{"status": "ok", "stdout": "def inspect(\n obj: Any,\n *,\n console: Optional[\"Console\"] = None,\n title: Optional[str] = None,\n help: bool = False,\n method
full
{"status": "ok", "stdout": "def inspect(\n    obj: Any,\n    *,\n    console: Optional[\"Console\"] = None,\n    title: Optional[str] = None,\n    help: bool = False,\n    methods: bool = False,\n    docs: bool = True,\n    private: bool = False,\n    dunder: bool = False,\n    sort: bool = True,\n    all: bool = False,\n    value: bool = True,\n) -> None:\n    \"\"\"Inspect any Python object.\n\n    * inspect(<OBJECT>) to see summarized info.\n    * inspect(<OBJECT>, methods=True) to see methods.\n    * inspect(<OBJECT>, help=True) to see full (non-abbreviated) help.\n    * inspect(<OBJECT>, private=True) to see private attributes (single underscore).\n    * inspect(<OBJECT>, dunder=True) to see attributes beginning with double underscore.\n    * inspect(<OBJECT>, all=True) to see all attributes.\n\n    Args:\n        obj (Any): An object to inspect.\n        title (str, optional): Title to display over inspect result, or None use type. Defaults to None.\n        help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.\n        methods (bool, optional): Enable inspection of callables. Defaults to False.\n        docs (bool, optional): Also render doc strings. Defaults to True.\n        private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.\n        dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.\n        sort (bool, optional): Sort attributes alphabetically. Defaults to True.\n        all (bool, optional): Show all attributes. Defaults to False.\n        value (bool, optional): Pretty print value. Defaults to True.\n    \"\"\"\n    _console = console or get_console()\n    from rich._inspect import Inspect\n\n    # Special case for inspect(inspect)\n    is_inspect = obj is inspect\n\n    _inspect = Inspect(\n        obj,\n        title=title,\n        help=is_inspect or help,\n        methods=is_inspect or methods,\n        docs=is_inspect o
4run_command
{
 "command": "# 4: Find Inspect class in rich/_inspect.py\ngrep -n \"class Inspect\" rich/_inspect.py\n"
}
{"status": "ok", "stdout": "21:class Inspect(JupyterMixin):\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "21:class Inspect(JupyterMixin):\n", "stderr": "", "exit_code": 0}
5run_command
{
 "command": "# 5: View Inspect.__init__ in rich/_inspect.py\nsed -n '21,100p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": "class Inspect(JupyterMixin):\n \"\"\"A renderable to inspect any Python Object.\n\n Args:\n obj (Any): An object to inspect.\n title
full
{"status": "ok", "stdout": "class Inspect(JupyterMixin):\n    \"\"\"A renderable to inspect any Python Object.\n\n    Args:\n        obj (Any): An object to inspect.\n        title (str, optional): Title to display over inspect result, or None use type. Defaults to None.\n        help (bool, optional): Show full help text rather than just first paragraph. Defaults to False.\n        methods (bool, optional): Enable inspection of callables. Defaults to False.\n        docs (bool, optional): Also render doc strings. Defaults to True.\n        private (bool, optional): Show private attributes (beginning with underscore). Defaults to False.\n        dunder (bool, optional): Show attributes starting with double underscore. Defaults to False.\n        sort (bool, optional): Sort attributes alphabetically. Defaults to True.\n        all (bool, optional): Show all attributes. Defaults to False.\n        value (bool, optional): Pretty print value of object. Defaults to True.\n    \"\"\"\n\n    def __init__(\n        self,\n        obj: Any,\n        *,\n        title: Optional[TextType] = None,\n        help: bool = False,\n        methods: bool = False,\n        docs: bool = True,\n        private: bool = False,\n        dunder: bool = False,\n        sort: bool = True,\n        all: bool = True,\n        value: bool = True,\n    ) -> None:\n        self.highlighter = ReprHighlighter()\n        self.obj = obj\n        self.title = title or self._make_title(obj)\n        if all:\n            methods = private = dunder = True\n        self.help = help\n        self.methods = methods\n        self.docs = docs or help\n        self.private = private or dunder\n        self.dunder = dunder\n        self.sort = sort\n        self.value = value\n\n    def _make_title(self, obj: Any) -> Text:\n        \"\"\"Make a default title.\"\"\"\n        title_str = (\n            str(obj)\n            if (isclass(obj) or callable(obj) or ismodule(obj))\n            else str(type(obj))\n     
6run_command
{
 "command": "# 6: Wait, _make_title uses str(obj) if it's a class, callable, or module.\n# But the error is \"Only str or Text can be appended to Text\".\n# This likely happens in the _render method or something that builds the title or the content.\n# Let's look at _render method in rich/_inspect.py\ngrep -n \"def _render\" rich/_inspect.py\n"
}
{"status": "ok", "stdout": "121: def _render(self) -> Iterable[RenderableType]:\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "121:    def _render(self) -> Iterable[RenderableType]:\n", "stderr": "", "exit_code": 0}
7run_command
{
 "command": "# 7: View _render method in rich/_inspect.py\nsed -n '121,200p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": " def _render(self) -> Iterable[RenderableType]:\n \"\"\"Render object.\"\"\"\n\n def sort_items(item: Tuple[str, Any]) -> Tuple[bool
full
{"status": "ok", "stdout": "    def _render(self) -> Iterable[RenderableType]:\n        \"\"\"Render object.\"\"\"\n\n        def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:\n            key, (_error, value) = item\n            return (callable(value), key.strip(\"_\").lower())\n\n        def safe_getattr(attr_name: str) -> Tuple[Any, Any]:\n            \"\"\"Get attribute or any exception.\"\"\"\n            try:\n                return (None, getattr(obj, attr_name))\n            except Exception as error:\n                return (error, None)\n\n        obj = self.obj\n        keys = dir(obj)\n        total_items = len(keys)\n        if not self.dunder:\n            keys = [key for key in keys if not key.startswith(\"__\")]\n        if not self.private:\n            keys = [key for key in keys if not key.startswith(\"_\")]\n        not_shown_count = total_items - len(keys)\n        items = [(key, safe_getattr(key)) for key in keys]\n        if self.sort:\n            items.sort(key=sort_items)\n\n        items_table = Table.grid(padding=(0, 1), expand=False)\n        items_table.add_column(justify=\"right\")\n        add_row = items_table.add_row\n        highlighter = self.highlighter\n\n        if callable(obj):\n            signature = self._get_signature(\"\", obj)\n            if signature is not None:\n                yield signature\n                yield \"\"\n\n        if self.docs:\n            _doc = self._get_formatted_doc(obj)\n            if _doc is not None:\n                doc_text = Text(_doc, style=\"inspect.help\")\n                doc_text = highlighter(doc_text)\n                yield doc_text\n                yield \"\"\n\n        if self.value and not (isclass(obj) or callable(obj) or ismodule(obj)):\n            yield Panel(\n                Pretty(obj, indent_guides=True, max_length=10, max_string=60),\n                border_style=\"inspect.value.border\",\n            )\n            yield \"\"\n\n        for key, (error, val
8run_command
{
 "command": "# 8: View more of _render method to see how it handles docs and signatures\nsed -n '201,250p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": " _signature_text.append(doc)\n\n add_row(key_text, _signature_text)\n else:\n add_row(key_text, Pretty(value, highlighter=highlighte
full
{"status": "ok", "stdout": "                            _signature_text.append(doc)\n\n                    add_row(key_text, _signature_text)\n            else:\n                add_row(key_text, Pretty(value, highlighter=highlighter))\n        if items_table.row_count:\n            yield items_table\n        elif not_shown_count:\n            yield Text.from_markup(\n                f\"[b cyan]{not_shown_count}[/][i] attribute(s) not shown.[/i] \"\n                f\"Run [b][magenta]inspect[/]([not b]inspect[/])[/b] for options.\"\n            )\n\n    def _get_formatted_doc(self, object_: Any) -> Optional[str]:\n        \"\"\"\n        Extract the docstring of an object, process it and returns it.\n        The processing consists in cleaning up the docstring's indentation,\n        taking only its 1st paragraph if `self.help` is not True,\n        and escape its control codes.\n\n        Args:\n            object_ (Any): the object to get the docstring from.\n\n        Returns:\n            Optional[str]: the processed docstring, or None if no docstring was found.\n        \"\"\"\n        docs = getdoc(object_)\n        if docs is None:\n            return None\n        docs = cleandoc(docs).strip()\n        if not self.help:\n            docs = _first_paragraph(docs)\n        return escape_control_codes(docs)\n\n\ndef get_object_types_mro(obj: Union[object, Type[Any]]) -> Tuple[type, ...]:\n    \"\"\"Returns the MRO of an object's class, or of the object itself if it's a class.\"\"\"\n    if not hasattr(obj, \"__mro__\"):\n        # N.B. we cannot use `if type(obj) is type` here because it doesn't work with\n        # some types of classes, such as the ones that use abc.ABCMeta.\n        obj = type(obj)\n    return getattr(obj, \"__mro__\", ())\n\n\ndef get_object_types_mro_as_strings(obj: object) -> Collection[str]:\n    \"\"\"\n    Returns the MRO of an object's class as full qualified names, or of the object itself if it's a class.\n\n    Examples:\n        `o
9run_command
{
 "command": "# 9: Let's look at get_object_types_mro_as_strings in rich/_inspect.py\nsed -n '251,270p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": " \"\"\"\n return [\n f'{getattr(type_, \"__module__\", \"\")}.{getattr(type_, \"__qualname__\", \"\")}'\n for type_ in get_object_ty
full
{"status": "ok", "stdout": "    \"\"\"\n    return [\n        f'{getattr(type_, \"__module__\", \"\")}.{getattr(type_, \"__qualname__\", \"\")}'\n        for type_ in get_object_types_mro(obj)\n    ]\n\n\ndef is_object_one_of_types(\n    obj: object, fully_qualified_types_names: Collection[str]\n) -> bool:\n    \"\"\"\n    Returns `True` if the given object's class (or the object itself, if it's a class) has one of the\n    fully qualified names in its MRO.\n    \"\"\"\n    for type_name in get_object_types_mro_as_strings(obj):\n        if type_name in fully_qualified_types_names:\n            return True\n    return False\n", "stderr": "", "exit_code": 0}
10run_command
{
 "command": "# 10: Let's check where get_object_types_mro_as_strings is used.\ngrep -rn \"get_object_types_mro_as_strings\" rich/_inspect.py\n"
}
{"status": "ok", "stdout": "rich/_inspect.py:245:def get_object_types_mro_as_strings(obj: object) -> Collection[str]:\nrich/_inspect.py:265: for type_name in ge
full
{"status": "ok", "stdout": "rich/_inspect.py:245:def get_object_types_mro_as_strings(obj: object) -> Collection[str]:\nrich/_inspect.py:265:    for type_name in get_object_types_mro_as_strings(obj):\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "# 11: Wait, the error is \"Only str or Text can be appended to Text\".\n# In _render, there is this line:\n# _signature_text.append(doc)\n# where _signature_text = self._get_signature(key, value).\n# Let's look at _get_signature again.\nsed -n '85,115p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": " _signature = str(signature(obj)) + \":\"\n except ValueError:\n _signature = \"(...)\"\n except TypeError:\n return None\n\n source
full
{"status": "ok", "stdout": "            _signature = str(signature(obj)) + \":\"\n        except ValueError:\n            _signature = \"(...)\"\n        except TypeError:\n            return None\n\n        source_filename: Optional[str] = None\n        try:\n            source_filename = getfile(obj)\n        except (OSError, TypeError):\n            # OSError is raised if obj has no source file, e.g. when defined in REPL.\n            pass\n\n        callable_name = Text(name, style=\"inspect.callable\")\n        if source_filename:\n            callable_name.stylize(f\"link file://{source_filename}\")\n        signature_text = self.highlighter(_signature)\n\n        qualname = name or getattr(obj, \"__qualname__\", name)\n\n        # If obj is a module, there may be classes (which are callable) to display\n        if inspect.isclass(obj):\n            prefix = \"class\"\n        elif inspect.iscoroutinefunction(obj):\n            prefix = \"async def\"\n        else:\n            prefix = \"def\"\n\n        qual_signature = Text.assemble(\n            (f\"{prefix} \", f\"inspect.{prefix.replace(' ', '_')}\"),\n            (qualname, \"inspect.callable\"),\n", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "# 12: Look at the end of _get_signature.\nsed -n '116,130p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": " signature_text,\n )\n\n return qual_signature\n\n def _render(self) -> Iterable[RenderableType]:\n \"\"\"Render object.\"\"\"\n\n d
full
{"status": "ok", "stdout": "            signature_text,\n        )\n\n        return qual_signature\n\n    def _render(self) -> Iterable[RenderableType]:\n        \"\"\"Render object.\"\"\"\n\n        def sort_items(item: Tuple[str, Any]) -> Tuple[bool, str]:\n            key, (_error, value) = item\n            return (callable(value), key.strip(\"_\").lower())\n\n        def safe_getattr(attr_name: str) -> Tuple[Any, Any]:\n            \"\"\"Get attribute or any exception.\"\"\"\n            try:\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "# 13: In _get_signature:\n# qualname = name or getattr(obj, \"__qualname__\", name)\n# Then:\n# qual_signature = Text.assemble(\n#     (f\"{prefix} \", f\"inspect.{prefix.replace(' ', '_')}\"),\n#     (qualname, \"inspect.callable\"),\n#     signature_text,\n# )\n# If qualname is not a string, Text.assemble will fail with \"Only str or Text can be appended to Text\".\n# This matches the error message.\n\n# The proposed fix is: check if __qualname__ is a str, fallback to __name__, then fallback to name.\n# Let's verify if this is the cause.\n# In the reproduction case, Klaas is a class decorated with @lru_cache.\n# When we call inspect(Klaas, all=True), it will iterate over attributes.\n# One of the attributes of Klaas is __qualname__.\n# When it reaches __qualname__, it calls _get_signature(\"__qualname__\", Klaas.__qualname__).\n# Wait, is Klaas.__qualname__ callable?\n# In the reproduction code:\n# @lru_cache\n# class Klaas:\n#     __slots__ = (\"__qualname__\",)\n#\n# In Python, if you decorate a class with @lru_cache (which is for functions), \n# lru_cache returns a wrapper. But here it's used as a class decorator.\n# lru_cache is not typically used on classes. \n# Let's see what happens when we run the reproduction code and print type(Klaas).\ncat > /tmp/debug_repro.py <<'PY'\nfrom functools import lru_cache\nfrom rich import inspect\nimport inspect as py_inspect\n\n@lru_cache\nclass Klaas:\n    __slots__ = (\"__qualname__\",)\n\nprint(f\"Klaas type: {type(Klaas)}\")\nprint(f\"Klaas.__qualname__ type: {type(Klaas.__qualname__)}\")\nprint(f\"Is Klaas callable: {callable(Klaas)}\")\n\n# Now let's see what happens inside rich.inspect when it's called with Klaas.\n# It iterates over attributes.\n# When it sees an attribute that is callable, it calls _get_signature.\nPY\npython3 /tmp/debug_repro.py\n"
}
{"status": "ok", "stdout": "Klaas type: <class 'functools._lru_cache_wrapper'>\nKlaas.__qualname__ type: <class 'member_descriptor'>\nIs Klaas callable: True\n"
full
{"status": "ok", "stdout": "Klaas type: <class 'functools._lru_cache_wrapper'>\nKlaas.__qualname__ type: <class 'member_descriptor'>\nIs Klaas callable: True\n", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "# 14: So Klaas is a _lru_cache_wrapper, which is callable.\n# When rich.inspect(Klaas) is called, it calls _get_signature(\"\", Klaas).\n# Inside _get_signature(\"\", Klaas):\n# name = \"\"\n# qualname = name or getattr(obj, \"__qualname__\", name)\n# Since name is \"\", qualname = getattr(Klaas, \"__qualname__\", \"\").\n# Klaas.__qualname__ is a member_descriptor.\n# Then Text.assemble is called with qualname (a member_descriptor), and it fails.\n\n# Let's verify this hypothesis by modifying the repro script to just call _get_signature.\ncat > /tmp/debug_get_signature.py <<'PY'\nfrom functools import lru_cache\nfrom rich._inspect import Inspect\n\n@lru_cache\nclass Klaas:\n    __slots__ = (\"__qualname__\",)\n\ninspect_obj = Inspect(Klaas)\ntry:\n    print(\"Calling _get_signature('', Klaas)...\")\n    inspect_obj._get_signature(\"\", Klaas)\n    print(\"Success!\")\nexcept Exception as e:\n    print(f\"Failed: {e}\")\nPY\npython3 /tmp/debug_get_signature.py\n"
}
{"status": "ok", "stdout": "Calling _get_signature('', Klaas)...\nFailed: Only str or Text can be appended to Text\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Calling _get_signature('', Klaas)...\nFailed: Only str or Text can be appended to Text\n", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "# 15: Now I will apply the fix.\n# Current code:\n# qualname = name or getattr(obj, \"__qualname__\", name)\n#\n# Proposed fix:\n# Check if __qualname__ is a str, fallback to __name__, then fallback to name.\n#\n# Let's see how to implement this.\n# We want:\n# 1. If name is provided, use it.\n# 2. If not, try getattr(obj, \"__qualname__\"). If it's a str, use it.\n# 3. If not a str, try getattr(obj, \"__name__\"). If it's a str, use it.\n# 4. Otherwise, use name (which might be empty string).\n#\n# Actually, the proposal in the issue was:\n# \"I fixed it by checking the type of __qualname__ falling back to __name__ and if that isn't a str using the already provided fallback.\"\n#\n# So:\n# qualname = name\n# if not qualname:\n#     qualname = getattr(obj, \"__qualname__\", None)\n#     if not isinstance(qualname, str):\n#         qualname = getattr(obj, \"__name__\", name)\n#         if not isinstance(qualname, str):\n#             qualname = name\n#\n# Or more concisely:\n# qualname = name\n# if not qualname:\n#     qualname = getattr(obj, \"__qualname__\", None)\n#     if not isinstance(qualname, str):\n#         qualname = getattr(obj, \"__name__\", None)\n#         if not isinstance(qualname, str):\n#             qualname = name\n#\n# Let's check the current implementation again.\n# qualname = name or getattr(obj, \"__qualname__\", name)\n#\n# I will replace this line with a more robust check.\n\n# Let's find the exact line number for the replacement.\ngrep -n \"qualname = name or getattr(obj, \\\"__qualname__\\\", name)\" rich/_inspect.py\n"
}
{"status": "ok", "stdout": "103: qualname = name or getattr(obj, \"__qualname__\", name)\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "103:        qualname = name or getattr(obj, \"__qualname__\", name)\n", "stderr": "", "exit_code": 0}
16run_command
{
 "command": "# 16: Let's implement the fix.\ncd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/_inspect.py\")\ns = p.read_text()\nold = '        qualname = name or getattr(obj, \"__qualname__\", name)'\nnew = \"\"\"        qualname = name\n        if not qualname:\n            qualname = getattr(obj, \"__qualname__\", None)\n            if not isinstance(qualname, str):\n                qualname = getattr(obj, \"__name__\", name)\n                if not isinstance(qualname, str):\n                    qualname = name\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
17run_command
{
 "command": "# 17: Verify the fix with the reproduction script.\npython3 /tmp/reproduce_issue.py\n"
}
{"status": "ok", "stdout": "Testing inspect(Klaas.__qualname__)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
full
{"status": "ok", "stdout": "Testing inspect(Klaas.__qualname__)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <class 'member_descriptor'> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\n\u2502 \u2502 <member '__qualname__' of 'Klaas' objects>               \u2502 \u2502\n\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u2502\n\u2502                                                              \u2502\n\u2502 30 attribute(s) not shown. Run inspect(inspect) for options. \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\nSuccess!\n\nTesting inspect(Klaas, all=True)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <functools._lru_cache_wrapper object at 0x1014697a0> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 def Klaas():                                   
18run_command
{
 "command": "# 18: The reproduction script now passes!\n# Let's double check the logic one more time.\n# The original code was:\n# qualname = name or getattr(obj, \"__qualname__\", name)\n#\n# If name is \"\", it takes getattr(obj, \"__qualname__\", \"\").\n# If __qualname__ is a member_descriptor, it is assigned to qualname.\n#\n# My fix:\n# qualname = name\n# if not qualname:\n#     qualname = getattr(obj, \"__qualname__\", None)\n#     if not isinstance(qualname, str):\n#         qualname = getattr(obj, \"__name__\", name)\n#         if not isinstance(qualname, str):\n#             qualname = name\n#\n# This ensures qualname is always a string (or whatever name was).\n#\n# Let's make sure we are not breaking any existing behavior.\n# If qualname is a str, it's used.\n# If not, but __name__ is a str, it's used.\n# If neither is a str, name is used.\n# This seems correct.\n\n# I'll just refine the code to be slightly cleaner.\ncd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/_inspect.py\")\ns = p.read_text()\nold = \"\"\"        qualname = name\n        if not qualname:\n            qualname = getattr(obj, \"__qualname__\", None)\n            if not isinstance(qualname, str):\n                qualname = getattr(obj, \"__name__\", name)\n                if not isinstance(qualname, str):\n                    qualname = name\"\"\"\nnew = \"\"\"        qualname = name\n        if not qualname:\n            qualname = getattr(obj, \"__qualname__\", None)\n            if not isinstance(qualname, str):\n                qualname = getattr(obj, \"__name__\", None)\n                if not isinstance(qualname, str):\n                    qualname = name\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new)\np.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
19run_command
{
 "command": "# 19: Final verification of the reproduction script.\npython3 /tmp/reproduce_issue.py\n"
}
{"status": "ok", "stdout": "Testing inspect(Klaas.__qualname__)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
full
{"status": "ok", "stdout": "Testing inspect(Klaas.__qualname__)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <class 'member_descriptor'> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 \u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e \u2502\n\u2502 \u2502 <member '__qualname__' of 'Klaas' objects>               \u2502 \u2502\n\u2502 \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f \u2502\n\u2502                                                              \u2502\n\u2502 30 attribute(s) not shown. Run inspect(inspect) for options. \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\nSuccess!\n\nTesting inspect(Klaas, all=True)...\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <functools._lru_cache_wrapper object at 0x101e757a0> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 def Klaas():                                   
20run_command
{
 "command": "# 20: Final check on file contents.\nsed -n '100,110p' rich/_inspect.py\n"
}
{"status": "ok", "stdout": " callable_name.stylize(f\"link file://{source_filename}\")\n signature_text = self.highlighter(_signature)\n\n qualname = name\n if
full
{"status": "ok", "stdout": "            callable_name.stylize(f\"link file://{source_filename}\")\n        signature_text = self.highlighter(_signature)\n\n        qualname = name\n        if not qualname:\n            qualname = getattr(obj, \"__qualname__\", None)\n            if not isinstance(qualname, str):\n                qualname = getattr(obj, \"__name__\", None)\n                if not isinstance(qualname, str):\n                    qualname = name\n\n", "stderr": "", "exit_code": 0}
21run_command
{
 "command": "# Remove reproduction scripts\nrm /tmp/reproduce_issue.py /tmp/debug_repro.py /tmp/debug_get_signature.py\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
22submit_patch
{}
{"status": "ok", "patch_size": 790, "files_changed": 1}
full
{"status": "ok", "patch_size": 790, "files_changed": 1}

Patch

diff --git a/rich/_inspect.py b/rich/_inspect.py
index 27d65cec..2a19a441 100644
--- a/rich/_inspect.py
+++ b/rich/_inspect.py
@@ -100,7 +100,13 @@ class Inspect(JupyterMixin):
             callable_name.stylize(f"link file://{source_filename}")
         signature_text = self.highlighter(_signature)
 
-        qualname = name or getattr(obj, "__qualname__", name)
+        qualname = name
+        if not qualname:
+            qualname = getattr(obj, "__qualname__", None)
+            if not isinstance(qualname, str):
+                qualname = getattr(obj, "__name__", None)
+                if not isinstance(qualname, str):
+                    qualname = name
 
         # If obj is a module, there may be classes (which are callable) to display
         if inspect.isclass(obj):

Test output

show
...s....sss...................................                           [100%]
42 passed, 4 skipped in 0.15s