Repository: Textualize/textual

Issue:
@work does not catch exceptions raised by calling repr on function's args
Consider this app, which calls the decorated `_job`, passing in an object that doesn't implement `repr`:

```python
from textual import work
from textual.app import App, ComposeResult
from textual.worker import WorkerFailed
from textual.widgets import Static

class NoRepr():
    def __repr__(self) -> str:
        raise NotImplementedError("Don't call repr!")

class Repro(App):

    def compose(self) -> ComposeResult:
        yield Static("hi")

    async def on_mount(self) -> None:
        foo = NoRepr()
        try:
            worker = self._job(foo)
            await worker.wait()
        except WorkerFailed as e:
            static = self.query_one(Static)
            static.renderable = "caught"

    @work(exclusive=False, exit_on_error=False)
    def _job(self, foo: NoRepr) -> None:
        return

if __name__ == "__main__":
    app = Repro()
    app.run()
```

Despite the try/catch block, this app crashes with this trace:

```
╭─────────────────────────────────────────────────────────── Traceback (most recent call last) ────────────────────────────────────────────────────────────╮
│ /home/tco/open/textual-bug-repro/repro.py:19 in on_mount                                                                                                 │
│                                                                                                                                                          │
│   16 │   async def on_mount(self) -> None:                                                    ╭────────────────────── locals ───────────────────────╮    │
│   17 │   │   foo = NoRepr()                                                                   │  foo = <repr-error "Don't call repr!">              │    │
│   18 │   │   try:                                                                             │ self = Repro(title='Repro', classes={'-dark-mode'}) │    │
│ ❱ 19 │   │   │   worker = self._job(foo)                                                      ╰─────────────────────────────────────────────────────╯    │
│   20 │   │   │   await worker.wait()                                                                                                                     │
│   21 │   │   except WorkerFailed as e:                                                                                                                   │
│   22 │   │   │   static = self.query_one(Static)                                                                                                         │
│                                                                                                                                                          │
│ /home/tco/.cache/pypoetry/virtualenvs/textual-bug-repro-JJcVc8Rx-py3.8/lib/python3.8/site-packages/textual/_work_decorator.py:90 in decorated            │
│                                                                                                                                                          │
│    87 │   │   │   self = args[0]                                                                                                                         │
│    88 │   │   │   assert isinstance(self, DOMNode)                                                                                                       │
│    89 │   │   │                                                                                                                                          │
│ ❱  90 │   │   │   positional_arguments = ", ".join(repr(arg) for arg in args[1:])                                                                        │
│    91 │   │   │   keyword_arguments = ", ".join(                                                                                                         │
│    92 │   │   │   │   f"{name}={value!r}" for name, value in kwargs.items()                                                                              │
│    93 │   │   │   )                                                                                                                                      │
│                                                                                                                                                          │
│ ╭──────────────────────────────────────────── locals ─────────────────────────────────────────────╮                                                      │
│ │          args = (Repro(title='Repro', classes={'-dark-mode'}), <repr-error "Don't call repr!">) │                                                      │
│ │       DOMNode = <class 'textual.dom.DOMNode'>                                                   │                                                      │
│ │     exclusive = False                                                                           │                                                      │
│ │ exit_on_error = False                                                                           │                                                      │
│ │         group = 'default'                                                                       │                                                      │
│ │        kwargs = {}                                                                              │                                                      │
│ │        method = <function Repro._job at 0x7f76d4e9ac10>                                         │                                                      │
│ │          name = ''                                                                              │                                                      │
│ │          self = Repro(title='Repro', classes={'-dark-mode'})                                    │                                                      │
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯                                                      │
│                                                                                                                                                          │
│ /home/tco/.cache/pypoetry/virtualenvs/textual-bug-repro-JJcVc8Rx-py3.8/lib/python3.8/site-packages/textual/_work_decorator.py:90 in <genexpr>            │
│                                                                                                                                                          │
│    87 │   │   │   self = args[0]                                                               ╭──────────────────── locals ─────────────────────╮       │
│    88 │   │   │   assert isinstance(self, DOMNode)                                             │  .0 = <tuple_iterator object at 0x7f76cdb42610> │       │
│    89 │   │   │                                                                                │ arg = <repr-error "Don't call repr!">           │       │
│ ❱  90 │   │   │   positional_arguments = ", ".join(repr(arg) for arg in args[1:])              ╰─────────────────────────────────────────────────╯       │
│    91 │   │   │   keyword_arguments = ", ".join(                                                                                                         │
│    92 │   │   │   │   f"{name}={value!r}" for name, value in kwargs.items()                                                                              │
│    93 │   │   │   )                                                                                                                                      │
│                                                                                                                                                          │
│ /home/tco/open/textual-bug-repro/repro.py:9 in __repr__                                                                                                  │
│                                                                                                                                                          │
│    6                                                                                          ╭──────────────── locals ────────────────╮                 │
│    7 class NoRepr():                                                                          │ self = <repr-error "Don't call repr!"> │                 │
│    8 │   def __repr__(self) -> str:                                                           ╰────────────────────────────────────────╯                 │
│ ❱  9 │   │   raise NotImplementedError("Don't call repr!")                                                                                               │
│   10                                                                                                                                                     │
│   11 class Repro(App):                                                                                                                                   │
│   12                                                                                                                                                     │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
NotImplementedError: Don't call repr!
```

This might seem trivial, but this bit me [here](https://github.com/tconbeer/harlequin/issues/56), since DuckDB's DuckDBPyRelations raise errors on `repr` for some queries:

```python
>>> import duckdb
>>> conn = duckdb.connect(":memory:")
>>> rel = conn.sql("select 0::struct(a int)")
>>> repr(rel)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
duckdb.ConversionException: Conversion Error: Unimplemented type for cast (INTEGER -> STRUCT(a INTEGER))
```

Consequentially, this app, which attempts to fetch data from a DuckDBPyRelation in a @work-decorated function, will crash on some queries unless an additional `except` block is added to catch the DuckDB errors:

```python
import duckdb
from textual import work
from textual.app import App, ComposeResult
from textual.worker import WorkerFailed, get_current_worker
from textual.widgets import Static

class Repro(App):

    def compose(self) -> ComposeResult:
        yield Static("hi")

    async def on_mount(self) -> None:
        conn = duckdb.connect(":memory:")
        rel = conn.sql("select 0::struct(a int)")
        try:
            worker = self.fetch_relation_data(rel)
            await worker.wait()
        except WorkerFailed as e:
            static = self.query_one(Static)
            static.renderable = f"WorkerFailed: {e.error}"
        # uncomment to stop crashes
        # except duckdb.Error as e:
        #     static = self.query_one(Static)
        #     static.renderable = f"DuckDB Error: {e}"

    def set_data(self, data) -> None:
        self.data = data

    @work(exclusive=True, exit_on_error=False)  # type: ignore
    def fetch_relation_data(self, relation: duckdb.DuckDBPyRelation) -> None:
        data = relation.fetchall()
        worker = get_current_worker()
        if not worker.is_cancelled:
            self.call_from_thread(self.set_data, data)

if __name__ == "__main__":
    app = Repro()
    app.run()
```

# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.24.1 |
| Rich    | 13.3.5 |

## Python

| Name           | Value                                                                             |
|----------------|-----------------------------------------------------------------------------------|
| Version        | 3.8.10                                                                            |
| Implementation | CPython                                                                           |
| Compiler       | GCC 9.3.0                                                                         |
| Executable     | /home/tco/.cache/pypoetry/virtualenvs/textual-bug-repro-JJcVc8Rx-py3.8/bin/python |

## Operating System

| Name    | Value                               |
|---------|-------------------------------------|
| System  | Linux                               |
| Release | 5.10.60.1-microsoft-standard-WSL2   |
| Version | #1 SMP Wed Aug 25 23:20:18 UTC 2021 |

## Terminal

| Name                 | Value            |
|----------------------|------------------|
| Terminal Application | Windows Terminal |
| TERM                 | xterm-256color   |
| COLORTERM            | *Not set*        |
| FORCE_COLOR          | *Not set*        |
| NO_COLOR             | *Not set*        |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=156, height=40 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 156                  |
| is_terminal    | True                 |
| encoding       | utf-8                |
| max_height     | 40                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |


Discussion:
Thank you for your issue. Give us a little time to review it.

PS. You might want to check the [FAQ](https://github.com/textualize/textual/blob/main/FAQ.md) if you haven't done so already.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)
I guess the solution would be to use a default string if the repr fails. It is only used as a debug aid anyway.

Resolving patch:
diff --git a/src/textual/_work_decorator.py b/src/textual/_work_decorator.py
--- a/src/textual/_work_decorator.py
+++ b/src/textual/_work_decorator.py
@@ -87,14 +87,15 @@ def decorated(
             self = args[0]
             assert isinstance(self, DOMNode)
 
-            positional_arguments = ", ".join(repr(arg) for arg in args[1:])
-            keyword_arguments = ", ".join(
-                f"{name}={value!r}" for name, value in kwargs.items()
-            )
-            tokens = [positional_arguments, keyword_arguments]
-            worker_description = (
-                f"{method.__name__}({', '.join(token for token in tokens if token)})"
-            )
+            try:
+                positional_arguments = ", ".join(repr(arg) for arg in args[1:])
+                keyword_arguments = ", ".join(
+                    f"{name}={value!r}" for name, value in kwargs.items()
+                )
+                tokens = [positional_arguments, keyword_arguments]
+                worker_description = f"{method.__name__}({', '.join(token for token in tokens if token)})"
+            except Exception:
+                worker_description = "<worker>"
             worker = cast(
                 "Worker[ReturnType]",
                 self.run_worker(


Test patch: