resolved RESOLVED PASS · 4 tool calls · 33 s · Textualize/rich
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 | Arguments | Result |
|---|---|---|---|
| 1 | run_command | {
"command": "python3 -c 'from functools import lru_cache\nfrom rich import inspect\n\n@lru_cache\nclass Klaas:\n __slots__ = (\"__qualname__\",)\n\ninspect(Klaas, all=True)'"
} | {"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recenfull{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n File \"<string>\", line 8, in <module>\n inspect(Klaas, all=True)\n ~~~~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/rich/__init__.py\", line 173, in inspect\n _console.print(_inspect)\n ~~~~~~~~~~~~~~^^^^^^^^^^\n File \"/workspace/rich/console.py\", line 1698, in print\n renderables = self._collect_renderables(\n objects,\n ...<5 lines>...\n highlight=highlight,\n )\n File \"/workspace/rich/console.py\", line 1555, in _collect_renderables\n renderable = rich_cast(renderable)\n File \"/workspace/rich/protocol.py\", line 36, in rich_cast\n renderable = cast_method()\n File \"/workspace/rich/_inspect.py\", line 76, in __rich__\n Group(*self._render()),\n ~~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/rich/_inspect.py\", line 153, in _render\n signature = self._get_signature(\"\", obj)\n File \"/workspace/rich/_inspect.py\", line 113, in _get_signature\n qual_signature = Text.assemble(\n (f\"{prefix} \", f\"inspect.{prefix.replace(' ', '_')}\"),\n (qualname, \"inspect.callable\"),\n signature_text,\n )\n File \"/workspace/rich/text.py\", line 397, in assemble\n append(*part)\n ~~~~~~^^^^^^^\n File \"/workspace/rich/text.py\", line 979, in append\n raise TypeError(\"Only str or Text can be appended to Text\")\nTypeError: Only str or Text can be appended to Text\n", "exit_code": 1}} |
| 2 | read_file | {
"filepath": "rich/_inspect.py",
"start_line": 100,
"end_line": 120
} | {"status": "ok", "filepath": "rich/_inspect.py", "content": " callable_name.stylize(f\"link file://{source_filename}\")\n signature_text = self.highlighter(_sigfull{"status": "ok", "filepath": "rich/_inspect.py", "content": " 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", "start_line": 100, "end_line": 120, "total_lines": 268, "is_truncated": false} |
| 3 | edit_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} |
| 4 | run_command | {
"command": "python3 -c 'from functools import lru_cache\nfrom rich import inspect\n\n@lru_cache\nclass Klaas:\n __slots__ = (\"__qualname__\",)\n\ninspect(Klaas, all=True)'"
} | {"status": "ok", "stdout": "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <functools._lru_cache_wrapper object at 0x78220b695850> \u2500\u2full{"status": "ok", "stdout": "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 <functools._lru_cache_wrapper object at 0x78220b695850> \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 def Klaas(): \u2502\n\u2502 \u2502\n\u2502 __annotations__ = {} \u2502\n\u2502 __dict__ = { \u2502\n\u2502 'cache_parameters': <function \u2502\n\u2502 lru_cache.<locals>.<lambda> at 0x78220b4a74c0>, \u2502\n\u2502 '__module__': '__main__', \u2502\n\u2502 '__name__': 'Klaas', \u2502\n\u2502 '__qualname__': <member '__qualname__' of \u2502\n\u2502 'Klaas' objects>, \u2502\n\u2502 '__doc__': None, \u2502\n\u2502 '__annotations__': {}, \u2502\n\u2502 '__type_params__': (), \u2502\n\u2502 '__firstlineno__': 4, \u2502\n\u2502 '__slots__': ('__qualname__',), \u2502\n\u2502 '__static_attributes__': (), \u2502\n\u2502 '__wrapped__': <class '__main__.Klaas'> \u2502\n\u2502 } \u2502\n\u2502 __doc__ = None \u2502\n\u2502 __firstlineno_ |
| 5 | submit_patch | [] | {"status": "ok", "patch_size": 604, "files_changed": 1}full{"status": "ok", "patch_size": 604, "files_changed": 1} |
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):
...s....sss................................... [100%] 42 passed, 4 skipped in 0.21s