Skip to content

Commit

Permalink
Fix error with "enqueue=True" and non-picklable exception (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
Delgan committed Jul 21, 2020
1 parent 6f86f48 commit 9a40e58
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 13 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
=============

- Fix ``AttributeError`` within handlers using ``serialize=True`` when calling ``logger.exception()`` outside of the context of an exception (`#296 <https://github.com/Delgan/loguru/issues/296>`_).
- Add support for async callable classes (with ``__call__`` method) used as sinks (`#294 <https://github.com/Delgan/loguru/issues/294>`_, thanks `@jessekrubin <https://github.com/jessekrubin>`_).
- Fix error while logging an exception containing a non-picklable ``value`` to a handler with ``enqueue=True`` (`#298 <https://github.com/Delgan/loguru/issues/298>`_).
- Add support for async callable classes (with ``__call__`` method) used as sinks (`#294 <https://github.com/Delgan/loguru/pull/294>`_, thanks `@jessekrubin <https://github.com/jessekrubin>`_).


`0.5.1`_ (2020-06-12)
Expand Down
18 changes: 16 additions & 2 deletions loguru/_recattrs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import pickle
from collections import namedtuple


Expand Down Expand Up @@ -63,5 +64,18 @@ def __repr__(self):
return "(type=%r, value=%r, traceback=%r)" % (self.type, self.value, self.traceback)

def __reduce__(self):
exception = (self.type, self.value, None) # tracebacks are not pickable
return (RecordException, exception)
try:
pickled_value = pickle.dumps(self.value)
except pickle.PickleError:
return (RecordException, (self.type, None, None))
else:
return (RecordException._from_pickled_value, (self.type, pickled_value, None))

@classmethod
def _from_pickled_value(cls, type_, pickled_value, traceback_):
try:
value = pickle.loads(pickled_value)
except pickle.PickleError:
return cls(type_, None, traceback_)
else:
return cls(type_, value, traceback_)
36 changes: 30 additions & 6 deletions tests/test_add_option_enqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import time
import re
import sys
import pickle


class NotPicklable:
def __getstate__(self):
raise RuntimeError("You shall not serialize me!")
raise pickle.PicklingError("You shall not serialize me!")

def __setstate__(self, state):
pass
Expand All @@ -18,7 +19,7 @@ def __getstate__(self):
return "..."

def __setstate__(self, state):
raise RuntimeError("You shall not de-serialize me!")
raise pickle.UnpicklingError("You shall not de-serialize me!")


class NotWritable:
Expand Down Expand Up @@ -80,7 +81,7 @@ def test_caught_exception_queue_put(writer, capsys):
assert out == ""
assert lines[0] == "--- Logging error in Loguru Handler #0 ---"
assert re.match(r"Record was: \{.*Bye bye.*\}", lines[1])
assert lines[-2] == "RuntimeError: You shall not serialize me!"
assert lines[-2].endswith("PicklingError: You shall not serialize me!")
assert lines[-1] == "--- End of logging error ---"


Expand All @@ -98,7 +99,7 @@ def test_caught_exception_queue_get(writer, capsys):
assert out == ""
assert lines[0] == "--- Logging error in Loguru Handler #0 ---"
assert lines[1] == "Record was: None"
assert lines[-2] == "RuntimeError: You shall not de-serialize me!"
assert lines[-2].endswith("UnpicklingError: You shall not de-serialize me!")
assert lines[-1] == "--- End of logging error ---"


Expand All @@ -124,7 +125,7 @@ def test_not_caught_exception_queue_put(writer, capsys):

logger.info("It's fine")

with pytest.raises(RuntimeError, match=r"You shall not serialize me!"):
with pytest.raises(pickle.PicklingError, match=r"You shall not serialize me!"):
logger.bind(broken=NotPicklable()).info("Bye bye...")

logger.remove()
Expand All @@ -149,7 +150,7 @@ def test_not_caught_exception_queue_get(writer, capsys):
assert writer.read() == "It's fine\n"
assert out == ""
assert lines[0].startswith("Exception")
assert lines[-1] == "RuntimeError: You shall not de-serialize me!"
assert lines[-1].endswith("UnpicklingError: You shall not de-serialize me!")


def test_not_caught_exception_sink_write(capsys):
Expand Down Expand Up @@ -183,3 +184,26 @@ def slow_sink(message):

assert out == ""
assert err == "".join("%d\n" % i for i in range(10))


@pytest.mark.parametrize("arg", [NotPicklable(), NotUnpicklable()])
def test_logging_not_picklable_exception(arg):
exception = None

def sink(message):
nonlocal exception
exception = message.record["exception"]

logger.add(sink, enqueue=True, catch=False)

try:
raise ValueError(arg)
except Exception:
logger.exception("Oups")

logger.remove()

type_, value, traceback_ = exception
assert type_ is ValueError
assert value is None
assert traceback_ is None
5 changes: 1 addition & 4 deletions tests/test_coroutine_sink.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ async def __call__(self, msg):
print(msg, end="")


async_writer_cls = AsyncWriter()


def test_coroutine_function(capsys):
async def worker():
logger.debug("A message")
Expand All @@ -44,7 +41,7 @@ async def worker():
logger.debug("A message")
await logger.complete()

logger.add(async_writer_cls, format="{message}")
logger.add(AsyncWriter(), format="{message}")

asyncio.run(worker())

Expand Down

0 comments on commit 9a40e58

Please sign in to comment.