Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: Delgan/loguru
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 0.7.1
Choose a base ref
...
head repository: Delgan/loguru
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 0.7.2
Choose a head ref
  • 14 commits
  • 71 files changed
  • 4 contributors

Commits on Sep 8, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    c5f2ac1 View commit details
  2. Add unit tests to validate exception formatting of new Python syntax

    These tests ensure that there are no unexpected errors during syntax
    highlighting and local variable collection. However, this also shows
    that the implementation can be improved. Some new keywords (such as
    "match") are not recognized.
    
    The addition of "--force-exclude" is necessary to prevent "black"
    formatter from failing (despite "fmt: off") due to a parsing error
    (caused by "--target-version py35" when the file contains a new
    syntax).
    Delgan committed Sep 8, 2023
    Copy the full SHA
    a91c1cc View commit details
  3. Copy the full SHA
    9da27db View commit details
  4. Bump pre-commit from 3.3.1 to 3.4.0 (#979)

    Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.3.1 to 3.4.0.
    - [Release notes](https://github.com/pre-commit/pre-commit/releases)
    - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
    - [Commits](pre-commit/pre-commit@v3.3.1...v3.4.0)
    
    ---
    updated-dependencies:
    - dependency-name: pre-commit
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 8, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    91351df View commit details

Commits on Sep 9, 2023

  1. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0f9cdeb View commit details
  2. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    db6c40b View commit details

Commits on Sep 11, 2023

  1. Fix possible truncated source while colorizing traceback in Python 3.12

    In particular, a "\" at the end of a line (indicating line
    continuation) would have been lost. This is because prior to Python
    3.12, the tokenizer would emit "ERRORTOKEN" (as the continuation of the
    line is not part of the traceback) but it would be colorized anyway.
    With Python 3.12, a "TokenError" is raised instead, which cause the
    token iteration to end prematurely. Because of a typo in the code, the
    final part of the source (usually empty, except if it couldn't be
    tokenized) wasn't added to the output string.
    Delgan committed Sep 11, 2023
    Copy the full SHA
    22bccb7 View commit details
  2. Fix f-string formatting in traceback of Python 3.12

    In Python 3.12, new tokens were added to "tokenize" module in order to
    differentiate simple strings and f-string. Additionally, the expressions
    inside the f-string are properly parsed as well. The unit tests have
    been updated consequently.
    Delgan committed Sep 11, 2023
    Copy the full SHA
    f1e94ab View commit details
  3. Fix deprecation of "datetime.utcfromoffset()"

    Although it's not recommended, we still use a naive datetime for UTC
    time (to calculate the local offset). I'm sure there's a more elegant
    solution, but as this is code for old, untestable platforms, I'd
    rather not radically change the implementation.
    Delgan committed Sep 11, 2023
    Copy the full SHA
    37a2db2 View commit details
  4. Copy the full SHA
    b28978e View commit details
  5. Copy the full SHA
    3086159 View commit details
  6. Copy the full SHA
    14fa062 View commit details
  7. Fix error using "set_start_method()" after "logger" import (#974)

    Calling "multiprocessing.get_context(method=None)" had the unexpected
    side effect of also fixing the global start method (which can't be
    changed afterwards).
    Delgan authored Sep 11, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    086126f View commit details
  8. Bump version to 0.7.2

    Delgan committed Sep 11, 2023
    Copy the full SHA
    e1f48c9 View commit details
Showing with 1,983 additions and 226 deletions.
  1. +1 −0 .github/workflows/tests.yml
  2. +1 −1 .pre-commit-config.yaml
  3. +10 −1 CHANGELOG.rst
  4. +1 −1 loguru/__init__.py
  5. +125 −34 loguru/_better_exceptions.py
  6. +3 −1 loguru/_datetime.py
  7. +9 −3 loguru/_handler.py
  8. +5 −5 loguru/_logger.py
  9. +1 −1 ruff.toml
  10. +5 −2 setup.py
  11. +2 −2 tests/exceptions/output/diagnose/attributes.txt
  12. +6 −6 tests/exceptions/output/diagnose/parenthesis.txt
  13. +8 −8 tests/exceptions/output/diagnose/source_multilines.txt
  14. +6 −6 tests/exceptions/output/diagnose/syntax_highlighting.txt
  15. +30 −0 tests/exceptions/output/modern/exception_group_catch.txt
  16. +18 −0 tests/exceptions/output/modern/f_string.txt
  17. +186 −0 tests/exceptions/output/modern/grouped_as_cause_and_context.txt
  18. +323 −0 tests/exceptions/output/modern/grouped_max_depth.txt
  19. +85 −0 tests/exceptions/output/modern/grouped_max_length.txt
  20. +150 −0 tests/exceptions/output/modern/grouped_nested.txt
  21. +100 −0 tests/exceptions/output/modern/grouped_simple.txt
  22. +136 −0 tests/exceptions/output/modern/grouped_with_cause_and_context.txt
  23. +20 −0 tests/exceptions/output/modern/match_statement.txt
  24. +117 −0 tests/exceptions/output/modern/notes.txt
  25. +20 −0 tests/exceptions/output/modern/positional_only_argument.txt
  26. +22 −0 tests/exceptions/output/modern/type_hints.txt
  27. +16 −0 tests/exceptions/output/modern/walrus_operator.txt
  28. +2 −2 tests/exceptions/output/others/exception_in_property.txt
  29. +30 −30 tests/exceptions/output/others/nested_with_reraise.txt
  30. +7 −7 tests/exceptions/output/ownership/assertion_from_lib.txt
  31. +7 −7 tests/exceptions/output/ownership/assertion_from_local.txt
  32. +12 −12 tests/exceptions/output/ownership/callback.txt
  33. +12 −12 tests/exceptions/output/ownership/catch_decorator.txt
  34. +9 −9 tests/exceptions/output/ownership/catch_decorator_from_lib.txt
  35. +9 −9 tests/exceptions/output/ownership/decorated_callback.txt
  36. +7 −7 tests/exceptions/output/ownership/direct.txt
  37. +7 −7 tests/exceptions/output/ownership/indirect.txt
  38. +7 −7 tests/exceptions/output/ownership/string_lib.txt
  39. +7 −7 tests/exceptions/output/ownership/syntaxerror.txt
  40. +0 −1 tests/exceptions/source/diagnose/attributes.py
  41. +0 −1 tests/exceptions/source/diagnose/parenthesis.py
  42. +0 −1 tests/exceptions/source/diagnose/source_multilines.py
  43. +0 −1 tests/exceptions/source/diagnose/syntax_highlighting.py
  44. +25 −0 tests/exceptions/source/modern/exception_group_catch.py
  45. +21 −0 tests/exceptions/source/modern/f_string.py
  46. +42 −0 tests/exceptions/source/modern/grouped_as_cause_and_context.py
  47. +26 −0 tests/exceptions/source/modern/grouped_max_depth.py
  48. +15 −0 tests/exceptions/source/modern/grouped_max_length.py
  49. +40 −0 tests/exceptions/source/modern/grouped_nested.py
  50. +41 −0 tests/exceptions/source/modern/grouped_simple.py
  51. +43 −0 tests/exceptions/source/modern/grouped_with_cause_and_context.py
  52. +21 −0 tests/exceptions/source/modern/match_statement.py
  53. +43 −0 tests/exceptions/source/modern/notes.py
  54. +23 −0 tests/exceptions/source/modern/positional_only_argument.py
  55. +23 −0 tests/exceptions/source/modern/type_hints.py
  56. +25 −0 tests/exceptions/source/modern/walrus_operator.py
  57. +0 −1 tests/exceptions/source/others/assertionerror_without_traceback.py
  58. +0 −1 tests/exceptions/source/others/exception_in_property.py
  59. +0 −1 tests/exceptions/source/others/nested_with_reraise.py
  60. +0 −1 tests/exceptions/source/ownership/assertion_from_lib.py
  61. +0 −1 tests/exceptions/source/ownership/assertion_from_local.py
  62. +0 −1 tests/exceptions/source/ownership/callback.py
  63. +0 −1 tests/exceptions/source/ownership/catch_decorator.py
  64. +0 −1 tests/exceptions/source/ownership/catch_decorator_from_lib.py
  65. +0 −1 tests/exceptions/source/ownership/decorated_callback.py
  66. +0 −1 tests/exceptions/source/ownership/direct.py
  67. +0 −1 tests/exceptions/source/ownership/indirect.py
  68. +0 −1 tests/exceptions/source/ownership/string_lib.py
  69. +0 −1 tests/exceptions/source/ownership/syntaxerror.py
  70. +30 −22 tests/test_add_option_context.py
  71. +43 −0 tests/test_exceptions_formatting.py
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ jobs:
- '3.9'
- '3.10'
- '3.11'
- 3.12-dev
- pypy-3.9
include:
- os: ubuntu-20.04
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ repos:
rev: 23.7.0
hooks:
- id: black
args: [-l, '100', --target-version, py35]
args: [-l, '100', --target-version, py35, --force-exclude, tests/exceptions/source/modern/*]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.286
hooks:
11 changes: 10 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
`0.7.2`_ (2023-09-11)
=====================

- Add support for formatting of ``ExceptionGroup`` errors (`#805 <https://github.com/Delgan/loguru/issues/805>`_).
- Fix possible ``RuntimeError`` when using ``multiprocessing.set_start_method()`` after importing the ``logger`` (`#974 <https://github.com/Delgan/loguru/issues/974>`_)
- Fix formatting of possible ``__notes__`` attached to an ``Exception`` (`#980 <https://github.com/Delgan/loguru/issues/980>`_).


`0.7.1`_ (2023-09-04)
=====================

@@ -213,7 +221,8 @@
Initial release.


.. _Unreleased: https://github.com/delgan/loguru/compare/0.7.1...master
.. _Unreleased: https://github.com/delgan/loguru/compare/0.7.2...master
.. _0.7.2: https://github.com/delgan/loguru/releases/tag/0.7.2
.. _0.7.1: https://github.com/delgan/loguru/releases/tag/0.7.1
.. _0.7.0: https://github.com/delgan/loguru/releases/tag/0.7.0
.. _0.6.0: https://github.com/delgan/loguru/releases/tag/0.6.0
2 changes: 1 addition & 1 deletion loguru/__init__.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
from ._logger import Core as _Core
from ._logger import Logger as _Logger

__version__ = "0.7.1"
__version__ = "0.7.2"

__all__ = ["logger"]

159 changes: 125 additions & 34 deletions loguru/_better_exceptions.py
Original file line number Diff line number Diff line change
@@ -10,6 +10,24 @@
import tokenize
import traceback

if sys.version_info >= (3, 11):

def is_exception_group(exc):
return isinstance(exc, ExceptionGroup)

else:
try:
from exceptiongroup import ExceptionGroup
except ImportError:

def is_exception_group(exc):
return False

else:

def is_exception_group(exc):
return isinstance(exc, ExceptionGroup)


class SyntaxHighlighter:
_default_style = {
@@ -28,6 +46,12 @@ class SyntaxHighlighter:
_builtins = set(dir(builtins))
_constants = {"True", "False", "None"}
_punctation = {"(", ")", "[", "]", "{", "}", ":", ",", ";"}
_strings = {tokenize.STRING}
_fstring_middle = None

if sys.version_info >= (3, 12):
_strings.update({tokenize.FSTRING_START, tokenize.FSTRING_MIDDLE, tokenize.FSTRING_END})
_fstring_middle = tokenize.FSTRING_MIDDLE

def __init__(self, style=None):
self._style = style or self._default_style
@@ -38,7 +62,12 @@ def highlight(self, source):
output = ""

for token in self.tokenize(source):
type_, string, start, end, line = token
type_, string, (start_row, start_column), (_, end_column), line = token

if type_ == self._fstring_middle:
# When an f-string contains "{{" or "}}", they appear as "{" or "}" in the "string"
# attribute of the token. However, they do not count in the column position.
end_column += string.count("{") + string.count("}")

if type_ == tokenize.NAME:
if string in self._constants:
@@ -56,23 +85,20 @@ def highlight(self, source):
color = style["operator"]
elif type_ == tokenize.NUMBER:
color = style["number"]
elif type_ == tokenize.STRING:
elif type_ in self._strings:
color = style["string"]
elif type_ == tokenize.COMMENT:
color = style["comment"]
else:
color = style["other"]

start_row, start_column = start
_, end_column = end

if start_row != row:
source = source[:column]
source = source[column:]
row, column = start_row, 0

if type_ != tokenize.ENCODING:
output += line[column:start_column]
output += color.format(string)
output += color.format(line[start_column:end_column])

column = end_column

@@ -140,6 +166,15 @@ def _get_lib_dirs():
paths = {sysconfig.get_path(name, scheme) for scheme in schemes for name in names}
return [os.path.abspath(path).lower() + os.sep for path in paths if path in sys.path]

@staticmethod
def _indent(text, count, *, prefix="| "):
if count == 0:
yield text
return
for line in text.splitlines(True):
indented = " " * count + prefix + line
yield indented.rstrip() + "\n"

def _get_char(self, char, default):
try:
char.encode(self._encoding)
@@ -344,7 +379,9 @@ def _format_locations(self, frames_lines, *, has_introduction):

yield frame

def _format_exception(self, value, tb, *, seen=None, is_first=False, from_decorator=False):
def _format_exception(
self, value, tb, *, seen=None, is_first=False, from_decorator=False, group_nesting=0
):
# Implemented from built-in traceback module:
# https://github.com/python/cpython/blob/a5b76167/Lib/traceback.py#L468
exc_type, exc_value, exc_traceback = type(value), value, tb
@@ -356,46 +393,73 @@ def _format_exception(self, value, tb, *, seen=None, is_first=False, from_decora

if exc_value:
if exc_value.__cause__ is not None and id(exc_value.__cause__) not in seen:
for text in self._format_exception(
exc_value.__cause__, exc_value.__cause__.__traceback__, seen=seen
):
yield text
yield from self._format_exception(
exc_value.__cause__,
exc_value.__cause__.__traceback__,
seen=seen,
group_nesting=group_nesting,
)
cause = "The above exception was the direct cause of the following exception:"
if self._colorize:
cause = self._theme["cause"].format(cause)
if self._diagnose:
yield "\n\n" + cause + "\n\n\n"
yield from self._indent("\n\n" + cause + "\n\n\n", group_nesting)
else:
yield "\n" + cause + "\n\n"
yield from self._indent("\n" + cause + "\n\n", group_nesting)

elif (
exc_value.__context__ is not None
and id(exc_value.__context__) not in seen
and not exc_value.__suppress_context__
):
for text in self._format_exception(
exc_value.__context__, exc_value.__context__.__traceback__, seen=seen
):
yield text
yield from self._format_exception(
exc_value.__context__,
exc_value.__context__.__traceback__,
seen=seen,
group_nesting=group_nesting,
)
context = "During handling of the above exception, another exception occurred:"
if self._colorize:
context = self._theme["context"].format(context)
if self._diagnose:
yield "\n\n" + context + "\n\n\n"
yield from self._indent("\n\n" + context + "\n\n\n", group_nesting)
else:
yield "\n" + context + "\n\n"
yield from self._indent("\n" + context + "\n\n", group_nesting)

is_grouped = is_exception_group(value)

if is_grouped and group_nesting == 0:
yield from self._format_exception(
value,
tb,
seen=seen,
group_nesting=1,
is_first=is_first,
from_decorator=from_decorator,
)
return

try:
tracebacklimit = sys.tracebacklimit
traceback_limit = sys.tracebacklimit
except AttributeError:
tracebacklimit = None
traceback_limit = None

frames, final_source = self._extract_frames(
exc_traceback, is_first, limit=tracebacklimit, from_decorator=from_decorator
exc_traceback, is_first, limit=traceback_limit, from_decorator=from_decorator
)
exception_only = traceback.format_exception_only(exc_type, exc_value)

error_message = exception_only[-1][:-1] # Remove last new line temporarily
# Determining the correct index for the "Exception: message" part in the formatted exception
# is challenging. This is because it might be preceded by multiple lines specific to
# "SyntaxError" or followed by various notes. However, we can make an educated guess based
# on the indentation; the preliminary context for "SyntaxError" is always indented, while
# the Exception itself is not. This allows us to identify the correct index for the
# exception message.
for error_message_index, part in enumerate(exception_only): # noqa: B007
if not part.startswith(" "):
break

error_message = exception_only[error_message_index][:-1] # Remove last new line temporarily

if self._colorize:
if ":" in error_message:
@@ -414,24 +478,51 @@ def _format_exception(self, value, tb, *, seen=None, is_first=False, from_decora

error_message = "\n" + error_message

exception_only[-1] = error_message + "\n"

frames_lines = traceback.format_list(frames) + exception_only
has_introduction = bool(frames)

if self._colorize or self._backtrace or self._diagnose:
frames_lines = self._format_locations(frames_lines, has_introduction=has_introduction)
exception_only[error_message_index] = error_message + "\n"

if is_first:
yield self._prefix

has_introduction = bool(frames)

if has_introduction:
introduction = "Traceback (most recent call last):"
if is_grouped:
introduction = "Exception Group Traceback (most recent call last):"
else:
introduction = "Traceback (most recent call last):"
if self._colorize:
introduction = self._theme["introduction"].format(introduction)
yield introduction + "\n"
if group_nesting == 1: # Implies we're processing the root ExceptionGroup.
yield from self._indent(introduction + "\n", group_nesting, prefix="+ ")
else:
yield from self._indent(introduction + "\n", group_nesting)

yield "".join(frames_lines)
frames_lines = traceback.format_list(frames) + exception_only
if self._colorize or self._backtrace or self._diagnose:
frames_lines = self._format_locations(frames_lines, has_introduction=has_introduction)

yield from self._indent("".join(frames_lines), group_nesting)

if is_grouped:
for n, exc in enumerate(value.exceptions, start=1):
ruler = "+" + (" %s " % ("..." if n > 15 else n)).center(35, "-")
yield from self._indent(ruler, group_nesting, prefix="+-" if n == 1 else " ")
if n > 15:
message = "and %d more exceptions\n" % (len(value.exceptions) - 15)
yield from self._indent(message, group_nesting + 1)
break
elif group_nesting == 10 and is_exception_group(exc):
message = "... (max_group_depth is 10)\n"
yield from self._indent(message, group_nesting + 1)
else:
yield from self._format_exception(
exc,
exc.__traceback__,
seen=seen,
group_nesting=group_nesting + 1,
)
if not is_exception_group(exc) or group_nesting == 10:
yield from self._indent("-" * 35, group_nesting + 1, prefix="+-")

def format_exception(self, type_, value, tb, *, from_decorator=False):
yield from self._format_exception(value, tb, is_first=True, from_decorator=from_decorator)
4 changes: 3 additions & 1 deletion loguru/_datetime.py
Original file line number Diff line number Diff line change
@@ -94,7 +94,9 @@ def aware_now():
seconds = local.tm_gmtoff
zone = local.tm_zone
except AttributeError:
offset = datetime_.fromtimestamp(timestamp) - datetime_.utcfromtimestamp(timestamp)
# Workaround for Python 3.5.
utc_naive = datetime_.fromtimestamp(timestamp, tz=timezone.utc).replace(tzinfo=None)
offset = datetime_.fromtimestamp(timestamp) - utc_naive
seconds = offset.total_seconds()
zone = strftime("%Z")

12 changes: 9 additions & 3 deletions loguru/_handler.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import json
import multiprocessing
import os
import threading
from contextlib import contextmanager
@@ -88,10 +89,15 @@ def __init__(
self._decolorized_format = self._formatter.strip()

if self._enqueue:
self._queue = self._multiprocessing_context.SimpleQueue()
if self._multiprocessing_context is None:
self._queue = multiprocessing.SimpleQueue()
self._confirmation_event = multiprocessing.Event()
self._confirmation_lock = multiprocessing.Lock()
else:
self._queue = self._multiprocessing_context.SimpleQueue()
self._confirmation_event = self._multiprocessing_context.Event()
self._confirmation_lock = self._multiprocessing_context.Lock()
self._queue_lock = create_handler_lock()
self._confirmation_event = self._multiprocessing_context.Event()
self._confirmation_lock = self._multiprocessing_context.Lock()
self._owner_process_pid = os.getpid()
self._thread = Thread(
target=self._queued_writer, daemon=True, name="loguru-writer-%d" % self._id
10 changes: 5 additions & 5 deletions loguru/_logger.py
Original file line number Diff line number Diff line change
@@ -85,7 +85,6 @@
import builtins
import contextlib
import functools
import itertools
import logging
import re
import sys
@@ -182,7 +181,7 @@ def __init__(self):
name: (name, name, level.no, level.icon) for name, level in self.levels.items()
}

self.handlers_count = itertools.count()
self.handlers_count = 0
self.handlers = {}

self.extra = {}
@@ -778,7 +777,8 @@ def add(
>>> logger.add(stream_object, level="INFO")
"""
with self._core.lock:
handler_id = next(self._core.handlers_count)
handler_id = self._core.handlers_count
self._core.handlers_count += 1

error_interceptor = ErrorInterceptor(catch, handler_id)

@@ -967,9 +967,9 @@ def add(
if not isinstance(encoding, str):
encoding = "ascii"

if context is None or isinstance(context, str):
if isinstance(context, str):
context = get_context(context)
elif not isinstance(context, BaseContext):
elif context is not None and not isinstance(context, BaseContext):
raise TypeError(
"Invalid context, it should be a string or a multiprocessing context, "
"not: '%s'" % type(context).__name__
2 changes: 1 addition & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Enforce pyflakes(F), pycodestyle(E, W), isort (I), bugbears (B), and pep8-naming (N) rules
select = ["F", "E", "W", "I", "B", "N"]
line-length = 100
exclude = ["tests/exceptions/source"]
exclude = ["tests/exceptions/source/*"]
[pycodestyle]
max-doc-length = 100
Loading