Repository: Delgan/loguru
Pull request #294 (2020-07-13 19:12:36)

added async callable sinks and tests for async callable sinks
Would be super nice to merge this in!

Also in contributing, would be nice if you had a 'format'/'lint' script so it is easier to contribute!

What formatter are you (@Delgan) using?

Delgan: Hey @jessekrubin, thanks for the PR. This looks like a good idea!

I noticed that the `__call__` attribute is not read-only. Consequently, there is one possible edge case (very contrived example):
```python
def foo():
    pass

async def afoo():
    pass

foo.__call__ = afoo

logger.add(foo)
```
In such case, the handler is going to end up in a broken state. It expects the sink to return a coroutine object, but calling `sink()` will execute the synchronous `foo()` function. I don't expect anyone to ever change `__call__` this way, but to prevent that from even being possible, what do you think of wrapping `sink.__call__` instead of `sink` directly?

Also, it's possible that accessing the `__call__` results in an `AttributeError` so I would like to handle that with `getattr()`.

```python
elif inspect.iscoroutinefunction(sink) or inspect.iscoroutinefunction(getattr(sink, "__call__", None)):
    ...

    if inspect.iscoroutinefunction(sink):
        coro = sink
    else:
        coro = sink.__call__

    wrapped_sink = AsyncSink(coro, loop, error_interceptor)
```

Also I don't think we need to copy/paste the whole `test_coroutine_sink.py` to `test_async_callable_sink.py`. It's a bloated enough file that I would prefer not to maintain twice. :smile: 

Adding a `test_async_callable_object()` similar to `test_coroutine_function()` should be enough, as well as the special cases I mentioned.

About formatting: I don't require any particular style for contributors. I run `black -l 100 .` myself from time to time. You're right, it's not documented. I'll add it to the `tox.ini` file.
jessekrubin: Howdy @Delgan I am down with all of your suggestions (it is your package(!) (and a mighty fine one at that!)). 

I get the not wanting to maintain 2 files.

Idk about adding it to tox (I think tox is a whole ordeal), I just was curious what you were doing for formatting as some people are very particular. 
		
Delgan: Thanks for the update! Actually, some files under `tests/` are not (yet) converted to `black` style (which broke comparisons between source and formatted exception), sorry you had to revert your commit. :sweat_smile: 

However, I'm still wondering... Why are the entire `_logger.py` and `test_coroutine_sink.y` files marked as changed in the PR `diff`, instead of just the few lines modified: https://github.com/Delgan/loguru/pull/294/files ? :confused: 
jessekrubin: > However, I'm still wondering... Why are the entire `_logger.py` and `test_coroutine_sink.y` files marked as changed in the PR `diff`, instead of just the few lines modified: [#294 (files)](https://github.com/Delgan/loguru/pull/294/files) ? 😕

Herm. That is p weird. I do not know! I have an `.editorconfig` in the dir above where I was working, so maybe it is that? (tho that has never happend to me before so I do not know)
jessekrubin: @Delgan I think i figured it out. it a combo of me working on windows, having a `.gitattributes` and a `.editorconfig` above where I was working. I can revert and try again. 
Delgan: Yeah, don't worry, I will make some tests next week-ends and we'll figure out something. :+1: 
jessekrubin: @Delgan I ACTUALLY FIGURED IT OUT! If you are curious. (it caused the same thing this afternoon in repo for my work)

I was working in a dir in a repo (where I cloned loguru). The repo above me had core.filemode set to true, so git was tracking the permissions which when changed cause git to think the whole file has changed.
Delgan: Thanks @jessekrubin for the investigation! :smile: 

Indeed, if we look at [the raw commit](https://github.com/Delgan/loguru/commit/c8af25177b2563a6ef967a78dc4639c87619fac7.diff) this is mentioned: `new file mode 100755`.
Github doesn't seem to display this information.

Patch:
diff --git a/loguru/_logger.py b/loguru/_logger.py
index 4834f49..388ee54 100644
--- a/loguru/_logger.py
+++ b/loguru/_logger.py
@@ -790,7 +790,9 @@ class Logger:
             encoding = getattr(sink, "encoding", None)
             terminator = ""
             exception_prefix = "\n"
-        elif inspect.iscoroutinefunction(sink):
+        elif inspect.iscoroutinefunction(sink) or inspect.iscoroutinefunction(
+            getattr(sink, "__call__", None)
+        ):
             name = getattr(sink, "__name__", None) or repr(sink)
 
             if colorize is None:
@@ -805,8 +807,8 @@ class Logger:
             # running loop in Python 3.5.2 and earlier versions, see python/asyncio#452.
             if enqueue and loop is None:
                 loop = asyncio.get_event_loop()
-
-            wrapped_sink = AsyncSink(sink, loop, error_interceptor)
+            coro = sink if inspect.iscoroutinefunction(sink) else sink.__call__
+            wrapped_sink = AsyncSink(coro, loop, error_interceptor)
             encoding = "utf8"
             terminator = "\n"
             exception_prefix = ""


Test patch:
diff --git a/tests/test_coroutine_sink.py b/tests/test_coroutine_sink.py
index 2b9266d..c270a43 100644
--- a/tests/test_coroutine_sink.py
+++ b/tests/test_coroutine_sink.py
@@ -16,6 +16,15 @@ async def async_writer(msg):
     print(msg, end="")
 
 
+class AsyncWriter:
+    async def __call__(self, msg):
+        await asyncio.sleep(0.01)
+        print(msg, end="")
+
+
+async_writer_cls = AsyncWriter()
+
+
 def test_coroutine_function(capsys):
     async def worker():
         logger.debug("A message")
@@ -30,6 +39,20 @@ def test_coroutine_function(capsys):
     assert out == "A message\n"
 
 
+def test_async_callable_sink(capsys):
+    async def worker():
+        logger.debug("A message")
+        await logger.complete()
+
+    logger.add(async_writer_cls, format="{message}")
+
+    asyncio.run(worker())
+
+    out, err = capsys.readouterr()
+    assert err == ""
+    assert out == "A message\n"
+
+
 def test_concurrent_execution(capsys):
     async def task(i):
         logger.debug("=> {}", i)