← eval8_submission_v1l

rich_3894

resolved RESOLVED PASS · 12 tool calls · 126 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 (13)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"inspect\" rich | grep \"def\" | head -20"
}
{"status": "ok", "stdout": "rich/__init__.py:120:def inspect(\nrich/default_styles.py:39: \"inspect.attr\": Style(color=\"yellow\", italic=True),\nrich/default_
full
{"status": "ok", "stdout": "rich/__init__.py:120:def inspect(\nrich/default_styles.py:39:    \"inspect.attr\": Style(color=\"yellow\", italic=True),\nrich/default_styles.py:40:    \"inspect.attr.dunder\": Style(color=\"yellow\", italic=True, dim=True),\nrich/default_styles.py:41:    \"inspect.callable\": Style(bold=True, color=\"red\"),\nrich/default_styles.py:42:    \"inspect.async_def\": Style(italic=True, color=\"bright_cyan\"),\nrich/default_styles.py:43:    \"inspect.def\": Style(italic=True, color=\"bright_cyan\"),\nrich/default_styles.py:44:    \"inspect.class\": Style(italic=True, color=\"bright_cyan\"),\nrich/default_styles.py:45:    \"inspect.error\": Style(bold=True, color=\"red\"),\nrich/default_styles.py:46:    \"inspect.equals\": Style(),\nrich/default_styles.py:47:    \"inspect.help\": Style(color=\"cyan\"),\nrich/default_styles.py:48:    \"inspect.doc\": Style(dim=True),\nrich/default_styles.py:49:    \"inspect.value.border\": Style(color=\"green\"),\nrich/pretty.py:109:    default_repr_file = inspect.getfile(_dummy_namedtuple.__repr__)\n", "stderr": "", "exit_code": 0}
2run_command
{
 "command": "grep -rn \"__qualname__\" rich | head -20"
}
{"status": "ok", "stdout": "rich/_inspect.py:103: qualname = name or getattr(obj, \"__qualname__\", name)\nrich/_inspect.py:253: f'{getattr(type_, \"__module__\
full
{"status": "ok", "stdout": "rich/_inspect.py:103:        qualname = name or getattr(obj, \"__qualname__\", name)\nrich/_inspect.py:253:        f'{getattr(type_, \"__module__\", \"\")}.{getattr(type_, \"__qualname__\", \"\")}'\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "sed -n '90,120p' rich/_inspect.py"
}
{"status": "ok", "stdout": "\n source_filename: Optional[str] = None\n try:\n source_filename = getfile(obj)\n except (OSError, TypeError):\n # OSError is raise
full
{"status": "ok", "stdout": "\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            signature_text,\n        )\n\n        return qual_signature\n\n", "stderr": "", "exit_code": 0}
4write_file
{
 "content": "from functools import lru_cache\nfrom rich import inspect\n\n@lru_cache\nclass Klaas:\n    __slots__ = (\"__qualname__\",)\n\nif __name__ == \"__main__\":\n    try:\n        inspect(Klaas.__qualname__)\n        print(\"inspect(Klaas.__qualname__) succeeded\")\n        inspect(Klaas, all=True)\n        print(\"inspect(Klaas, all=True) succeeded\")\n    except Exception as e:\n        print(f\"Failed with: {e}\")\n        import traceback\n        traceback.print_exc()\n",
 "filepath": "/tmp/repro.py"
}
{"status": "error", "error_type": "FileWriteError", "error_message": "Path traversal detected: '/tmp/repro.py' escapes workspace root."}
full
{"status": "error", "error_type": "FileWriteError", "error_message": "Path traversal detected: '/tmp/repro.py' escapes workspace root."}
5write_file
{
 "content": "from functools import lru_cache\nfrom rich import inspect\n\n@lru_cache\nclass Klaas:\n    __slots__ = (\"__qualname__\",)\n\nif __name__ == \"__main__\":\n    try:\n        inspect(Klaas.__qualname__)\n        print(\"inspect(Klaas.__qualname__) succeeded\")\n        inspect(Klaas, all=True)\n        print(\"inspect(Klaas, all=True) succeeded\")\n    except Exception as e:\n        print(f\"Failed with: {e}\")\n        import traceback\n        traceback.print_exc()\n",
 "filepath": "repro.py"
}
{"status": "ok", "filepath": "repro.py", "size": 446}
full
{"status": "ok", "filepath": "repro.py", "size": 446}
6run_command
{
 "command": "python repro.py"
}
{"status": "ok", "stdout": "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <class 'member_descriptor'> \
full
{"status": "ok", "stdout": "\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\ninspect(Klaas.__qualname__) succeeded\nFailed with: Only str or Text can be appended to Text\n", "stderr": "Traceback (most recent call last):\n  File \"/workspace/repro.py\", line 12, in <module>\n    inspect(Klaas, all=True)\n    ~~~~~~~^^^^^^^^^^^^^^^^^\n  File \"/workspace/rich/__init__.py\", line 173, in inspect\n    _console.print(_inspect)\n    ~
7run_command
{
 "command": "sed -n '100,115p' rich/_inspect.py"
}
{"status": "ok", "stdout": " callable_name.stylize(f\"link file://{source_filename}\")\n signature_text = self.highlighter(_signature)\n\n qualname = name or ge
full
{"status": "ok", "stdout": "            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}
8edit_file
{
 "filepath": "rich/_inspect.py",
 "new_string": "        qualname = name or getattr(obj, \"__qualname__\", name)\n        if not isinstance(qualname, str):\n            qualname = getattr(obj, \"__name__\", name)\n            if not isinstance(qualname, str):\n                qualname = str(qualname)",
 "old_string": "        qualname = name or getattr(obj, \"__qualname__\", name)"
}
{"status": "ok", "filepath": "rich/_inspect.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/rich/_inspect.py\n+++ b/rich/_inspect.py\n@@ -101,6 +101,
full
{"status": "ok", "filepath": "rich/_inspect.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/rich/_inspect.py\n+++ b/rich/_inspect.py\n@@ -101,6 +101,10 @@\n         signature_text = self.highlighter(_signature)\n \n         qualname = name or getattr(obj, \"__qualname__\", name)\n+        if not isinstance(qualname, str):\n+            qualname = getattr(obj, \"__name__\", name)\n+            if not isinstance(qualname, str):\n+                qualname = str(qualname)\n \n         # If obj is a module, there may be classes (which are callable) to display\n         if inspect.isclass(obj):\n", "is_truncated": false}
9run_command
{
 "command": "python repro.py"
}
{"status": "ok", "stdout": "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <class 'member_descriptor'> \
full
{"status": "ok", "stdout": "\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\ninspect(Klaas.__qualname__) succeeded\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <functools._lru_cache_wrapper object at 0x7e3bfd319590> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 def Klaas():                                                                 \u2502\n\u2502                     
10run_command
{
 "command": "python -m pytest tests/test_inspect.py -q"
}
{"status": "ok", "stdout": "...s....sss.................................. [100%]\n41 passed, 4 skipped in 0.20s\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "...s....sss..................................                            [100%]\n41 passed, 4 skipped in 0.20s\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "cd /workspace && git status --short && git diff --stat"
}
{"status": "ok", "stdout": " M rich/_inspect.py\n?? repro.py\n rich/_inspect.py | 4 ++++\n 1 file changed, 4 insertions(+)\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": " M rich/_inspect.py\n?? repro.py\n rich/_inspect.py | 4 ++++\n 1 file changed, 4 insertions(+)\n", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "python -m pytest tests/test_inspect.py -q -x -k inspect"
}
{"status": "ok", "stdout": "...s....sss.................................. [100%]\n41 passed, 4 skipped in 6.08s\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "...s....sss..................................                            [100%]\n41 passed, 4 skipped in 6.08s\n", "stderr": "", "exit_code": 0}
13submit_patch
{}
{"status": "ok", "patch_size": 1192, "files_changed": 2}
full
{"status": "ok", "patch_size": 1192, "files_changed": 2}

Patch

diff --git a/repro.py b/repro.py
new file mode 100644
index 00000000..1beee843
--- /dev/null
+++ b/repro.py
@@ -0,0 +1,17 @@
+from functools import lru_cache
+from rich import inspect
+
+@lru_cache
+class Klaas:
+    __slots__ = ("__qualname__",)
+
+if __name__ == "__main__":
+    try:
+        inspect(Klaas.__qualname__)
+        print("inspect(Klaas.__qualname__) succeeded")
+        inspect(Klaas, all=True)
+        print("inspect(Klaas, all=True) succeeded")
+    except Exception as e:
+        print(f"Failed with: {e}")
+        import traceback
+        traceback.print_exc()
diff --git a/rich/_inspect.py b/rich/_inspect.py
index 27d65cec..cace272d 100644
--- a/rich/_inspect.py
+++ b/rich/_inspect.py
@@ -101,6 +101,10 @@ class Inspect(JupyterMixin):
         signature_text = self.highlighter(_signature)
 
         qualname = name or getattr(obj, "__qualname__", name)
+        if not isinstance(qualname, str):
+            qualname = getattr(obj, "__name__", name)
+            if not isinstance(qualname, str):
+                qualname = str(qualname)
 
         # 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.22s