resolved RESOLVED PASS · 21 tool calls · 164 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": "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\ufull{"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} |
| 2 | run_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} |
| 3 | run_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 methodfull{"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 |
| 4 | run_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} |
| 5 | run_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 titlefull{"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 |
| 6 | run_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} |
| 7 | run_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[boolfull{"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 |
| 8 | run_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=highlightefull{"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 |
| 9 | run_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_tyfull{"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} |
| 10 | run_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 gefull{"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} |
| 11 | run_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 sourcefull{"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} |
| 12 | run_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 dfull{"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} |
| 13 | run_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} |
| 14 | run_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} |
| 15 | run_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} |
| 16 | run_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} |
| 17 | run_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\ufull{"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(): |
| 18 | run_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} |
| 19 | run_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\ufull{"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(): |
| 20 | run_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} |
| 21 | run_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} |
| 22 | submit_patch | {} | {"status": "ok", "patch_size": 790, "files_changed": 1}full{"status": "ok", "patch_size": 790, "files_changed": 1} |
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):
...s....sss................................... [100%] 42 passed, 4 skipped in 0.15s