Repository: Delgan/loguru

Issue:
Make `.catch()` context manager async compatible
Hi,

First, thanks for a lot for loguru :)

I have a piece of code like this:

```python
import httpx
from loguru import logger as loguru_logger

@app.get("/")  # some fastapi route
async def my_route(foo, bar):
    logger = loguru_logger.bind(foo=foo, bar=bar)  # add some context to my logger
    with logger.catch():  # first context manager
        async with httpx.AsyncClient() as client:  # second context manager
            r = await client.get('https://www.example.com/')
            # do stuff...
```

In python, it's possible to chain context managers: 
```python
with A() as X, B() as Y, C() as Z:
    do_something()
```

But I don't think it's possible to mix async and sync context managers.

So I wonder if we could make the class `Catcher` async compatible? I think that it would require implementing `__aenter__` and `__aexit__`

This feature would allow this:

```python
import httpx
from loguru import logger as loguru_logger

@app.get("/")  # some fastapi route
async def my_route(foo, bar):
    logger = loguru_logger.bind(foo=foo, bar=bar)  # add some context to my logger
    async with logger.catch(), httpx.AsyncClient() as client:  # all in one line
        r = await client.get('https://www.example.com/')
        # do stuff...
```

Thanks


Discussion:
Hey @Jonas1312. Thanks for the suggestion, it should be relatively easy to implement.

However, I'm wondering if it's good practice to make `logger.catch()` compatible with `async`, when it has nothing to do with `async` (it's just syntactic sugar for `try / except`)?

I suppose it shouldn't be too much of a concern, but I'd be interested to know of other libraries or built-ins context managers that have such behavior.
Yes I guess it would be relatively easy to implement:
- `__aenter__` would be empty
- `__aexit__` would call `__exit__`, followed by a `await logger.complete()` maybe?

For now, I'm making loguru catch compatible with async like this:
```python
@asynccontextmanager
async def catch_error(logger: "loguru.Logger") -> AsyncGenerator[None, None]:
    with logger.catch(reraise=True):
        yield

logger = loguru_logger.bind(foo=foo, bar=bar)  # add some context to my logger
async with catch_error(logger), httpx.AsyncClient() as client:  # all in one line
    r = await client.get('https://www.example.com/')
    # do stuff...
```

But that would be cleaner if loguru could do it natively.

There’s nothing dangerous about calling synchronous functions inside asynchronous ones (except blocking the event loop...), so it should be safe
Sorry for not answering sooner, and thanks for the example.

I'm re-opening this issue because I'll likely try to implement it, assuming I'll not face any technical complication.



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1f575b8b7..1d48efc70 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,7 @@
 - Update the default log format to include the timezone offset since it produces less ambiguous logs (`#856 <https://github.com/Delgan/loguru/pull/856>`_, thanks `@tim-x-y-z <https://github.com/tim-x-y-z>`_).
 - Honor the ``NO_COLOR`` environment variable to disable color output by default if ``colorize`` is not provided (`#1178 <https://github.com/Delgan/loguru/issues/1178>`_).
 - Add requirement for ``TERM`` environment variable not to be ``"dumb"`` to enable colorization (`#1287 <https://github.com/Delgan/loguru/pull/1287>`_, thanks `@snosov1 <https://github.com/snosov1>`_).
+- Make ``logger.catch()`` usable as an asynchronous context manager (`#1084 <https://github.com/Delgan/loguru/issues/1084>`_).
 - Make ``logger.catch()`` compatible with asynchronous generators (`#1302 <https://github.com/Delgan/loguru/issues/1302>`_).
 
 
diff --git a/loguru/_logger.py b/loguru/_logger.py
index 5c2ba54a5..7f17f1a9a 100644
--- a/loguru/_logger.py
+++ b/loguru/_logger.py
@@ -1333,6 +1333,12 @@ def catch_wrapper(*args, **kwargs):
                 functools.update_wrapper(catch_wrapper, function)
                 return catch_wrapper
 
+            async def __aenter__(self):
+                return self.__enter__()
+
+            async def __aexit__(self, type_, value, traceback_):
+                return self.__exit__(type_, value, traceback_)
+
         return Catcher(False)
 
     def opt(


Test patch:
diff --git a/tests/test_exceptions_catch.py b/tests/test_exceptions_catch.py
index 71ebc2917..f06b83181 100644
--- a/tests/test_exceptions_catch.py
+++ b/tests/test_exceptions_catch.py
@@ -439,6 +439,15 @@ async def foo():
     assert asyncio.run(foo()) == 42
 
 
+def test_async_context_manager():
+    async def coro():
+        async with logger.catch():
+            return 1 / 0
+        return 1
+
+    assert asyncio.run(coro()) == 1
+
+
 def test_error_when_decorating_class_without_parentheses():
     with pytest.raises(TypeError):