From 007307b03a65af2ac1bf15c498b966d0d02a989f Mon Sep 17 00:00:00 2001
From: DEBREUVE Eric <eric.debreuve@cnrs.fr>
Date: Fri, 7 Feb 2025 17:31:56 +0100
Subject: [PATCH] better management of rich presence, refactoring, better
 exception hook

---
 package/logger_36/api/logger.py               |  2 +-
 package/logger_36/api/storage.py              |  2 +-
 .../logger_36/catalog/config/console_rich.py  |  4 +-
 .../logger.py => catalog/config/optional.py}  | 56 +++------------
 package/logger_36/catalog/handler/console.py  |  9 ++-
 .../logger_36/catalog/handler/console_rich.py | 19 ++++--
 package/logger_36/catalog/handler/file.py     |  8 ++-
 package/logger_36/catalog/handler/generic.py  | 18 +++--
 package/logger_36/catalog/logger/gpu.py       |  4 +-
 package/logger_36/constant/error.py           |  2 +-
 package/logger_36/constant/handler.py         |  4 +-
 package/logger_36/constant/logger.py          | 11 +--
 package/logger_36/content.py                  |  6 +-
 package/logger_36/exception.py                | 58 ++++++++++++----
 package/logger_36/gpu.py                      |  2 +-
 package/logger_36/handler.py                  | 27 ++++----
 package/logger_36/memory.py                   | 15 ++--
 package/logger_36/storage.py                  |  2 +-
 package/logger_36/system.py                   |  2 +-
 package/logger_36/task/format/rule.py         |  6 +-
 package/logger_36/task/measure/memory.py      |  4 +-
 package/logger_36/task/storage.py             | 12 ++--
 package/logger_36/time.py                     |  4 +-
 package/logger_36/type/handler.py             | 27 ++++++--
 package/logger_36/type/logger.py              | 68 +++++++++++++++----
 package/logger_36/version.py                  |  2 +-
 test/exception.py                             |  5 +-
 27 files changed, 229 insertions(+), 150 deletions(-)
 rename package/logger_36/{config/logger.py => catalog/config/optional.py} (59%)

diff --git a/package/logger_36/api/logger.py b/package/logger_36/api/logger.py
index 906dfca..f45bc0b 100644
--- a/package/logger_36/api/logger.py
+++ b/package/logger_36/api/logger.py
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.type.logger import logger_t
+from logger_36.type.logger import logger_t  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/api/storage.py b/package/logger_36/api/storage.py
index ada69d3..9802750 100644
--- a/package/logger_36/api/storage.py
+++ b/package/logger_36/api/storage.py
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.task.storage import html_reader_t
+from logger_36.task.storage import html_reader_t  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/catalog/config/console_rich.py b/package/logger_36/catalog/config/console_rich.py
index bd09679..838e3f5 100644
--- a/package/logger_36/catalog/config/console_rich.py
+++ b/package/logger_36/catalog/config/console_rich.py
@@ -6,8 +6,8 @@ SEE COPYRIGHT NOTICE BELOW
 
 import logging as l
 
-from rich.color import Color as color_t
-from rich.style import Style as style_t
+from rich.color import Color as color_t  # noqa
+from rich.style import Style as style_t  # noqa
 
 """
 Colors: See https://rich.readthedocs.io/en/stable/appendix/colors.html.
diff --git a/package/logger_36/config/logger.py b/package/logger_36/catalog/config/optional.py
similarity index 59%
rename from package/logger_36/config/logger.py
rename to package/logger_36/catalog/config/optional.py
index 41a29ae..a0f50e0 100644
--- a/package/logger_36/config/logger.py
+++ b/package/logger_36/catalog/config/optional.py
@@ -4,54 +4,14 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-import logging as l
-
-from logger_36.catalog.handler.console import console_handler_t
-from logger_36.catalog.handler.console_rich import console_rich_handler_t
-from logger_36.catalog.handler.file import file_handler_t
-from logger_36.catalog.handler.generic import generic_handler_t
-from logger_36.constant.handler import HANDLER_CODES, handler_codes_h
-from logger_36.instance.logger import L
-from logger_36.task.format.message import MessageWithActualExpected
-
-
-def SetLOGLevel(
-    level: int,
-    /,
-    *,
-    logger: l.Logger = L,
-    which: handler_codes_h | str = "a",
-) -> None:
-    """
-    which: g=generic, c=console, f=file, a=all, str=name.
-    """
-    which_is_name = which not in HANDLER_CODES
-    found = False
-    for handler in logger.handlers:
-        if (
-            (which == "a")
-            or ((which == "g") and isinstance(handler, generic_handler_t))
-            or (
-                (which == "c")
-                and isinstance(handler, (console_handler_t, console_rich_handler_t))
-            )
-            or ((which == "f") and isinstance(handler, file_handler_t))
-            or (which == handler.name)
-        ):
-            handler.setLevel(level)
-            if which_is_name:
-                return
-            found = True
-
-    if not found:
-        raise ValueError(
-            MessageWithActualExpected(
-                "Handler not found",
-                actual=which,
-                expected=f"{str(HANDLER_CODES)[1:-1]}, or a handler name",
-            )
-        )
-
+try:
+    import rich  # noqa
+except ModuleNotFoundError:
+    RICH_IS_AVAILABLE = False
+    from logger_36.constant.error import MISSING_RICH_MESSAGE
+else:
+    RICH_IS_AVAILABLE = True
+    MISSING_RICH_MESSAGE = None
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/catalog/handler/console.py b/package/logger_36/catalog/handler/console.py
index 31fb26b..ca990e7 100644
--- a/package/logger_36/catalog/handler/console.py
+++ b/package/logger_36/catalog/handler/console.py
@@ -13,12 +13,19 @@ from logger_36.constant.message import LINE_INDENT
 from logger_36.constant.record import SHOW_W_RULE_ATTR
 from logger_36.task.format.rule import RuleAsText
 from logger_36.type.handler import handler_extension_t
+from logger_36.type.handler import message_from_record_raw_p as message_from_record_p
 
 
 @d.dataclass(slots=True, repr=False, eq=False)
 class console_handler_t(l.Handler):
+    """
+    kind: See logger_36.constant.handler.handler_codes_h.
+    """
+
+    kind: h.ClassVar[str] = "c"
+
     extension: handler_extension_t = d.field(init=False)
-    MessageFromRecord: h.Callable[..., tuple[str, str | None]] = d.field(init=False)
+    MessageFromRecord: message_from_record_p = d.field(init=False)
 
     name: d.InitVar[str | None] = None
     level: d.InitVar[int] = l.NOTSET
diff --git a/package/logger_36/catalog/handler/console_rich.py b/package/logger_36/catalog/handler/console_rich.py
index 3e62bca..46ec039 100644
--- a/package/logger_36/catalog/handler/console_rich.py
+++ b/package/logger_36/catalog/handler/console_rich.py
@@ -24,11 +24,14 @@ from logger_36.constant.message import CONTEXT_LENGTH, LINE_INDENT
 from logger_36.constant.record import SHOW_W_RULE_ATTR
 from logger_36.task.format.rule import Rule, rule_t
 from logger_36.type.handler import handler_extension_t
-from rich.console import Console as console_t
-from rich.console import RenderableType as renderable_t
-from rich.markup import escape as EscapedVersion
-from rich.text import Text as text_t
-from rich.traceback import install as InstallTracebackHandler
+from logger_36.type.handler import (
+    message_from_record_preprocessed_p as message_from_record_p,
+)
+from rich.console import Console as console_t  # noqa
+from rich.console import RenderableType as renderable_t  # noqa
+from rich.markup import escape as EscapedVersion  # noqa
+from rich.text import Text as text_t  # noqa
+from rich.traceback import install as InstallTracebackHandler  # noqa
 
 _COMMON_TRACEBACK_ARGUMENTS = ("theme", "width")
 _EXCLUSIVE_TRACEBACK_ARGUMENTS = (
@@ -47,6 +50,8 @@ _EXCLUSIVE_TRACEBACK_ARGUMENTS = (
 @d.dataclass(slots=True, repr=False, eq=False)
 class console_rich_handler_t(l.Handler):
     """
+    kind: See logger_36.constant.handler.handler_codes_h.
+
     alternating_lines:
     - Initial value:
         - 1: enabled for dark background
@@ -55,9 +60,11 @@ class console_rich_handler_t(l.Handler):
     - Runtime value: 0/1=do not/do highlight next time.
     """
 
+    kind: h.ClassVar[str] = "c"
+
     extension: handler_extension_t = d.field(init=False)
     console: console_t = d.field(init=False)
-    MessageFromRecord: h.Callable[..., str] = d.field(init=False)
+    MessageFromRecord: message_from_record_p = d.field(init=False)
     alternating_lines: int = 0
     background_is_light: bool = True
 
diff --git a/package/logger_36/catalog/handler/file.py b/package/logger_36/catalog/handler/file.py
index 96f3a7e..a73ef49 100644
--- a/package/logger_36/catalog/handler/file.py
+++ b/package/logger_36/catalog/handler/file.py
@@ -14,13 +14,19 @@ from logger_36.constant.message import LINE_INDENT
 from logger_36.constant.record import SHOW_W_RULE_ATTR
 from logger_36.task.format.rule import RuleAsText
 from logger_36.type.handler import handler_extension_t
+from logger_36.type.handler import message_from_record_raw_p as message_from_record_p
 
 
 @d.dataclass(slots=True, repr=False, eq=False)
 class file_handler_t(l.FileHandler):
+    """
+    kind: See logger_36.constant.handler.handler_codes_h.
+    """
+
+    kind: h.ClassVar[str] = "f"
 
     extension: handler_extension_t = d.field(init=False)
-    MessageFromRecord: h.Callable[..., tuple[str, str | None]] = d.field(init=False)
+    MessageFromRecord: message_from_record_p = d.field(init=False)
 
     name: d.InitVar[str | None] = None
     level: d.InitVar[int] = l.NOTSET
diff --git a/package/logger_36/catalog/handler/generic.py b/package/logger_36/catalog/handler/generic.py
index 0f743d9..5914602 100644
--- a/package/logger_36/catalog/handler/generic.py
+++ b/package/logger_36/catalog/handler/generic.py
@@ -8,19 +8,23 @@ import dataclasses as d
 import logging as l
 import typing as h
 
-try:
+from logger_36.catalog.config.optional import RICH_IS_AVAILABLE
+
+if RICH_IS_AVAILABLE:
     from logger_36.catalog.config.console_rich import DATE_TIME_COLOR
     from logger_36.catalog.handler.console_rich import HighlightedVersion
     from rich.console import Console as console_t  # noqa
     from rich.console import ConsoleOptions as console_options_t  # noqa
     from rich.markup import escape as EscapedForRich  # noqa
     from rich.terminal_theme import DEFAULT_TERMINAL_THEME  # noqa
-except ModuleNotFoundError:
-    console_t = console_options_t = EscapedForRich = DEFAULT_TERMINAL_THEME = None
+else:
+    DATE_TIME_COLOR = HighlightedVersion = console_t = console_options_t = (
+        EscapedForRich
+    ) = DEFAULT_TERMINAL_THEME = None
 
 from logger_36.constant.record import SHOW_W_RULE_ATTR
 from logger_36.task.format.rule import Rule, RuleAsText
-from logger_36.type.handler import handler_extension_t
+from logger_36.type.handler import handler_extension_t, message_from_record_h
 
 
 @h.runtime_checkable
@@ -36,6 +40,8 @@ class display_rule_p(h.Protocol):
 @d.dataclass(slots=True, repr=False, eq=False)
 class generic_handler_t(l.Handler):
     """
+    kind: See logger_36.constant.handler.handler_codes_h.
+
     alternating_lines:
         - Initial value:
             - 1: enabled for dark background
@@ -54,6 +60,8 @@ class generic_handler_t(l.Handler):
         it is indeed called at the end of the emit method.
     """
 
+    kind: h.ClassVar[str] = "g"
+
     ShowMessage: show_message_p
     # "None -> h.Any" (twice below) since None | None is invalid.
     console: console_t | h.Any = None
@@ -63,7 +71,7 @@ class generic_handler_t(l.Handler):
 
     DisplayRule: display_rule_p = d.field(init=False)
     extension: handler_extension_t = d.field(init=False)
-    MessageFromRecord: h.Callable[..., tuple[str, str | None]] = d.field(init=False)
+    MessageFromRecord: message_from_record_h = d.field(init=False)
 
     name: d.InitVar[str | None] = None
     level: d.InitVar[int] = l.NOTSET
diff --git a/package/logger_36/catalog/logger/gpu.py b/package/logger_36/catalog/logger/gpu.py
index bc855ee..2cc37d9 100644
--- a/package/logger_36/catalog/logger/gpu.py
+++ b/package/logger_36/catalog/logger/gpu.py
@@ -13,11 +13,11 @@ from logger_36.type.logger import logger_t
 try:
     import tensorflow as tsfl  # noqa
     import tensorrt as tsrt  # noqa
-
-    _GPU_LOGGING_ERROR = None
 except ModuleNotFoundError:
     tsfl = tsrt = None
     _GPU_LOGGING_ERROR = GPU_LOGGING_ERROR
+else:
+    _GPU_LOGGING_ERROR = None
 
 
 def LogGPURelatedDetails(*, logger: logger_t = L) -> None:
diff --git a/package/logger_36/constant/error.py b/package/logger_36/constant/error.py
index 1c7d6d4..8a741a8 100644
--- a/package/logger_36/constant/error.py
+++ b/package/logger_36/constant/error.py
@@ -16,7 +16,7 @@ MEMORY_MEASURE_ERROR = (
     "is not installed or not importable."
 )
 
-MISSING_RICH_ERROR = (
+MISSING_RICH_MESSAGE = (
     "The Rich console handler is not available because the Rich package "
     "(https://rich.readthedocs.io/en/stable/) "
     "is not installed or not importable. "
diff --git a/package/logger_36/constant/handler.py b/package/logger_36/constant/handler.py
index f323b62..34ddb1d 100644
--- a/package/logger_36/constant/handler.py
+++ b/package/logger_36/constant/handler.py
@@ -6,8 +6,8 @@ SEE COPYRIGHT NOTICE BELOW
 
 import typing as h
 
-handler_codes_h = h.Literal["g", "c", "f", "a"]
-HANDLER_CODES: tuple[str, ...] = h.get_args(handler_codes_h)
+handler_codes_h = h.Literal["g", "c", "f", "a"]  # g=generic, c=console, f=file, a=all.
+HANDLER_KINDS: tuple[str, ...] = h.get_args(handler_codes_h)
 
 ANONYMOUS = "<Anonymous>"
 
diff --git a/package/logger_36/constant/logger.py b/package/logger_36/constant/logger.py
index 53d39e7..870a8eb 100644
--- a/package/logger_36/constant/logger.py
+++ b/package/logger_36/constant/logger.py
@@ -4,21 +4,14 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-import logging as l
-import re as regx
-import typing as h
+import re as r
 
 LOGGER_NAME = "logger-36"
 
 # https://docs.python.org/3/library/logging.html#logging.captureWarnings
 WARNING_LOGGER_NAME = "py.warnings"
 WARNING_TYPE_PATTERN = r"\s*([^:]+):([0-9]+):\s*([^:]+)\s*:((.|\n)*)"
-WARNING_TYPE_COMPILED_PATTERN = regx.compile(WARNING_TYPE_PATTERN)
-
-# Second version: with self as first parameter.
-logger_handle_h = (
-    h.Callable[[l.LogRecord], None] | h.Callable[[l.Logger, l.LogRecord], None]
-)
+WARNING_TYPE_COMPILED_PATTERN = r.compile(WARNING_TYPE_PATTERN)
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/content.py b/package/logger_36/content.py
index 4e82a16..b50d82e 100644
--- a/package/logger_36/content.py
+++ b/package/logger_36/content.py
@@ -4,9 +4,9 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.constant.message import LINE_INDENT
-from logger_36.task.format.message import MessageWithActualExpected
-from logger_36.task.format.rule import Rule, RuleAsText
+from logger_36.constant.message import LINE_INDENT  # noqa
+from logger_36.task.format.message import MessageWithActualExpected  # noqa
+from logger_36.task.format.rule import Rule, RuleAsText  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/exception.py b/package/logger_36/exception.py
index 2066fdc..0b82c1e 100644
--- a/package/logger_36/exception.py
+++ b/package/logger_36/exception.py
@@ -4,6 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
+import re as r
 import sys as s
 import tempfile as tmpf
 import traceback as tcbk
@@ -30,24 +31,57 @@ def _HandleException(
     while trace.tb_next is not None:
         trace = trace.tb_next
     frame = trace.tb_frame
-    module = frame.f_code.co_filename
-    function = frame.f_code.co_name
-    line = frame.f_lineno
-
-    home = str(path_t.home())
-    if module.startswith(home):
-        module = "~" + module[home.__len__() :]
-
-    message = str(exception)
+    code = frame.f_code
+    module = path_t(code.co_filename)
+    function = code.co_name
+    line_number = frame.f_lineno
+    line_content = module.read_text().splitlines()[line_number - 1].strip()
+
+    # Format module.
+    home = path_t.home()
+    if module.is_relative_to(home):
+        module = path_t("~") / module.relative_to(home)
+
+    # Format line content.
+    if line_content.startswith("raise "):
+        # Do not display code of explicit exception raising.
+        line_content = None
+
+    # Find variables appearing in the line.
+    if line_content is None:
+        line_content = variables = ""
+    else:
+        all_variables = frame.f_locals
+        found_names = []
+        for match in r.finditer(r"[^\d\W]\w*", line_content):
+            name = match.group()
+            if name in all_variables:
+                found_names.append(name)
+        if found_names.__len__() > 0:
+            longest = max(map(len, found_names))
+            variables = map(
+                lambda _: f"{_:{longest}} = {all_variables[_]}", sorted(found_names)
+            )
+            variables = "        " + "\n        ".join(variables) + "\n"
+        else:
+            variables = ""
+
+        line_content = f"    {line_content}\n"
+
+    # Format message.
+    message = str(exception).strip()
     if message.__len__() > 0:
-        message = f"    {message}\n"
+        message = f"    {message[0].title()}{message[1:]}\n"
 
     document = tmpf.NamedTemporaryFile(delete=False)
 
     print(
         f"{stripe.__name__}\n"
-        f"    {module}.{function}@{line}\n"
-        f"{message}    Full report at: {document.name}",
+        f"    {module}:{function}@{line_number}\n"
+        f"{line_content}"
+        f"{variables}"
+        f"{message}"
+        f"    Full report at: {document.name}",
         file=s.stderr,
     )
 
diff --git a/package/logger_36/gpu.py b/package/logger_36/gpu.py
index af0df2d..b1477a6 100644
--- a/package/logger_36/gpu.py
+++ b/package/logger_36/gpu.py
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.catalog.logger.gpu import LogGPURelatedDetails
+from logger_36.catalog.logger.gpu import LogGPURelatedDetails  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/handler.py b/package/logger_36/handler.py
index c5a6532..c09b904 100644
--- a/package/logger_36/handler.py
+++ b/package/logger_36/handler.py
@@ -8,21 +8,18 @@ import logging as l
 import sys as s
 from pathlib import Path as path_t
 
+from logger_36.catalog.config.optional import MISSING_RICH_MESSAGE, RICH_IS_AVAILABLE
 from logger_36.catalog.handler.console import console_handler_t
 from logger_36.catalog.handler.file import file_handler_t
 from logger_36.catalog.handler.generic import generic_handler_t, show_message_p
-from logger_36.constant.error import MISSING_RICH_ERROR
 
-try:
+if RICH_IS_AVAILABLE:
     from logger_36.catalog.handler.console_rich import console_rich_handler_t
-
-    _MISSING_RICH_ERROR = None
-except ModuleNotFoundError:
+else:
     from logger_36.catalog.handler.console import (
         console_handler_t as console_rich_handler_t,
     )
-
-    _MISSING_RICH_ERROR = MISSING_RICH_ERROR
+_MISSING_RICH_MESSAGE = MISSING_RICH_MESSAGE
 
 
 def AddGenericHandler(
@@ -54,7 +51,7 @@ def AddGenericHandler(
         rich_kwargs=kwargs,
         ShowMessage=ShowMessage,
     )
-    logger.AddHandler(handler, should_hold_messages)
+    logger.AddHandler(handler, should_hold_messages=should_hold_messages)
 
 
 def AddConsoleHandler(
@@ -76,7 +73,7 @@ def AddConsoleHandler(
         message_width=message_width,
         formatter=formatter,
     )
-    logger.AddHandler(handler, should_hold_messages)
+    logger.AddHandler(handler, should_hold_messages=should_hold_messages)
 
 
 def AddRichConsoleHandler(
@@ -95,10 +92,10 @@ def AddRichConsoleHandler(
     **kwargs,
 ) -> None:
     """"""
-    global _MISSING_RICH_ERROR
-    if _MISSING_RICH_ERROR is not None:
-        print(_MISSING_RICH_ERROR, file=s.stderr)
-        _MISSING_RICH_ERROR = None
+    global _MISSING_RICH_MESSAGE
+    if _MISSING_RICH_MESSAGE is not None:
+        print(_MISSING_RICH_MESSAGE, file=s.stderr)
+        _MISSING_RICH_MESSAGE = None
 
     if console_rich_handler_t is console_handler_t:
         additional_s = {}
@@ -117,7 +114,7 @@ def AddRichConsoleHandler(
         formatter=formatter,
         **additional_s,
     )
-    logger.AddHandler(handler, should_hold_messages)
+    logger.AddHandler(handler, should_hold_messages=should_hold_messages)
 
 
 def AddFileHandler(
@@ -149,7 +146,7 @@ def AddFileHandler(
         handler_args=args,
         handler_kwargs=kwargs,
     )
-    logger.AddHandler(handler, should_hold_messages)
+    logger.AddHandler(handler, should_hold_messages=should_hold_messages)
 
 
 """
diff --git a/package/logger_36/memory.py b/package/logger_36/memory.py
index 40d5590..d8fdc4a 100644
--- a/package/logger_36/memory.py
+++ b/package/logger_36/memory.py
@@ -4,14 +4,17 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.catalog.logger.memory import LogMaximumMemoryUsage, LogMemoryUsages
-from logger_36.task.format.memory import FormattedUsage as FormattedMemoryUsage
-from logger_36.task.format.memory import (
+from logger_36.catalog.logger.memory import (  # noqa
+    LogMaximumMemoryUsage,
+    LogMemoryUsages,
+)
+from logger_36.task.format.memory import FormattedUsage as FormattedMemoryUsage  # noqa
+from logger_36.task.format.memory import (  # noqa
     FormattedUsageWithAutoUnit as FormattedMemoryUsageWithAutoUnit,
 )
-from logger_36.task.format.memory import UsageBar as MemoryUsageBar
-from logger_36.task.measure.memory import CanCheckUsage as CanCheckMemoryUsage
-from logger_36.task.measure.memory import CurrentUsage as CurrentMemoryUsage
+from logger_36.task.format.memory import UsageBar as MemoryUsageBar  # noqa
+from logger_36.task.measure.memory import CanCheckUsage as CanCheckMemoryUsage  # noqa
+from logger_36.task.measure.memory import CurrentUsage as CurrentMemoryUsage  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/storage.py b/package/logger_36/storage.py
index f5d15d0..3dd694f 100644
--- a/package/logger_36/storage.py
+++ b/package/logger_36/storage.py
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.task.storage import SaveLOGasHTML
+from logger_36.task.storage import SaveLOGasHTML  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/system.py b/package/logger_36/system.py
index 00c1b9a..4599d5b 100644
--- a/package/logger_36/system.py
+++ b/package/logger_36/system.py
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.catalog.logger.system import LogSystemDetails
+from logger_36.catalog.logger.system import LogSystemDetails  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/task/format/rule.py b/package/logger_36/task/format/rule.py
index 70a8dff..c3a80ab 100644
--- a/package/logger_36/task/format/rule.py
+++ b/package/logger_36/task/format/rule.py
@@ -4,6 +4,8 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
+from logger_36.catalog.config.optional import RICH_IS_AVAILABLE
+
 
 def RuleAsText(text: str | None, /) -> str:
     """"""
@@ -13,7 +15,7 @@ def RuleAsText(text: str | None, /) -> str:
         return f"---- ---- ---- ---- {text} ---- ---- ---- ----"
 
 
-try:
+if RICH_IS_AVAILABLE:
     from rich.rule import Rule as rule_t  # noqa
     from rich.text import Text as text_t  # noqa
 
@@ -24,7 +26,7 @@ try:
         else:
             return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
 
-except ModuleNotFoundError:
+else:
     Rule = lambda _txt, _: RuleAsText(_txt)
 
 """
diff --git a/package/logger_36/task/measure/memory.py b/package/logger_36/task/measure/memory.py
index 903346f..33b7713 100644
--- a/package/logger_36/task/measure/memory.py
+++ b/package/logger_36/task/measure/memory.py
@@ -6,10 +6,10 @@ SEE COPYRIGHT NOTICE BELOW
 
 try:
     from psutil import Process as process_t  # noqa
-
-    _PROCESS = process_t()
 except ModuleNotFoundError:
     _PROCESS = None
+else:
+    _PROCESS = process_t()
 
 
 def CanCheckUsage() -> bool:
diff --git a/package/logger_36/task/storage.py b/package/logger_36/task/storage.py
index cf79fe9..cfe82f2 100644
--- a/package/logger_36/task/storage.py
+++ b/package/logger_36/task/storage.py
@@ -6,17 +6,19 @@ SEE COPYRIGHT NOTICE BELOW
 
 import dataclasses as d
 import logging as l
-import re as regx
+import re as r
 from html.parser import HTMLParser as html_parser_t
 from io import IOBase as io_base_t
 from pathlib import Path as path_t
 
-try:
+from logger_36.catalog.config.optional import RICH_IS_AVAILABLE
+from logger_36.instance.logger import L
+
+if RICH_IS_AVAILABLE:
     from rich.console import Console as console_t  # noqa
-except ModuleNotFoundError:
+else:
     console_t = None
 
-from logger_36.instance.logger import L
 
 _BODY_END_PATTERN = r"</[bB][oO][dD][yY]>(.|\n)*$"
 
@@ -60,7 +62,7 @@ class html_reader_t(html_parser_t):
             output[self.body_position_start[0] : (self.body_position_end[0] + 1)]
         )
         output = output[self.body_position_start[1] :]
-        output = regx.sub(_BODY_END_PATTERN, "", output, count=1)
+        output = r.sub(_BODY_END_PATTERN, "", output, count=1)
 
         return output.strip()
 
diff --git a/package/logger_36/time.py b/package/logger_36/time.py
index d79b049..af0820d 100644
--- a/package/logger_36/time.py
+++ b/package/logger_36/time.py
@@ -4,8 +4,8 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-from logger_36.catalog.logger.chronos import LogElapsedTime
-from logger_36.task.measure.chronos import ElapsedTime, TimeStamp
+from logger_36.catalog.logger.chronos import LogElapsedTime  # noqa
+from logger_36.task.measure.chronos import ElapsedTime, TimeStamp  # noqa
 
 """
 COPYRIGHT NOTICE
diff --git a/package/logger_36/type/handler.py b/package/logger_36/type/handler.py
index 88f1c0d..7f40840 100644
--- a/package/logger_36/type/handler.py
+++ b/package/logger_36/type/handler.py
@@ -16,12 +16,31 @@ from logger_36.config.message import (
     WHERE_SEPARATOR,
 )
 from logger_36.constant.error import MEMORY_MEASURE_ERROR
-from logger_36.constant.handler import HANDLER_CODES
+from logger_36.constant.handler import HANDLER_KINDS
 from logger_36.constant.message import NEXT_LINE_PROLOGUE
 from logger_36.task.format.message import MessageWithActualExpected
 from logger_36.task.measure.chronos import TimeStamp
 from logger_36.task.measure.memory import CanCheckUsage as CanCheckMemoryUsage
 
+
+@h.runtime_checkable
+class message_from_record_raw_p(h.Protocol):
+    def __call__(self, record: l.LogRecord, /) -> str: ...
+
+
+@h.runtime_checkable
+class message_from_record_preprocessed_p(h.Protocol):
+    def __call__(
+        self,
+        record: l.LogRecord,
+        /,
+        *,
+        PreProcessed: h.Callable[[str], str] | None = None,
+    ) -> str: ...
+
+
+message_from_record_h = message_from_record_raw_p | message_from_record_preprocessed_p
+
 _MEMORY_MEASURE_ERROR = MEMORY_MEASURE_ERROR
 
 
@@ -30,7 +49,7 @@ class handler_extension_t:
     name: str | None = None
     should_store_memory_usage: bool = False
     message_width: int = -1
-    MessageFromRecord: h.Callable[..., str] = d.field(init=False)
+    MessageFromRecord: message_from_record_h = d.field(init=False)
 
     handler: d.InitVar[l.Handler | None] = None
     level: d.InitVar[int] = l.NOTSET
@@ -42,12 +61,12 @@ class handler_extension_t:
         """"""
         global _MEMORY_MEASURE_ERROR
 
-        if self.name in HANDLER_CODES:
+        if self.name in HANDLER_KINDS:
             raise ValueError(
                 MessageWithActualExpected(
                     "Invalid handler name",
                     actual=self.name,
-                    expected=f"a name not in {str(HANDLER_CODES)[1:-1]}",
+                    expected=f"a name not in {str(HANDLER_KINDS)[1:-1]}",
                 )
             )
 
diff --git a/package/logger_36/type/logger.py b/package/logger_36/type/logger.py
index 888d726..b44a9ed 100644
--- a/package/logger_36/type/logger.py
+++ b/package/logger_36/type/logger.py
@@ -24,13 +24,12 @@ from logger_36.config.message import (
     TIME_FORMAT,
 )
 from logger_36.constant.generic import NOT_PASSED
-from logger_36.constant.handler import ANONYMOUS
+from logger_36.constant.handler import ANONYMOUS, HANDLER_KINDS, handler_codes_h
 from logger_36.constant.issue import ISSUE_LEVEL_SEPARATOR, ORDER, order_h
 from logger_36.constant.logger import (
     LOGGER_NAME,
     WARNING_LOGGER_NAME,
     WARNING_TYPE_COMPILED_PATTERN,
-    logger_handle_h,
 )
 from logger_36.constant.memory import UNKNOWN_MEMORY_USAGE
 from logger_36.constant.message import TIME_LENGTH_m_1, expected_op_h
@@ -46,14 +45,18 @@ from logger_36.task.measure.chronos import ElapsedTime
 from logger_36.task.measure.memory import CurrentUsage as CurrentMemoryUsage
 from logger_36.type.issue import NewIssue, issue_t
 
-logger_base_t = l.Logger
+base_t = l.Logger
+
+logger_handle_raw_h = h.Callable[[l.LogRecord], None]
+logger_handle_with_self_h = h.Callable[[l.Logger, l.LogRecord], None]
+logger_handle_h = logger_handle_raw_h | logger_handle_with_self_h
 
 _DATE_TIME_ORIGIN = date_time_t.fromtimestamp(1970, None)
 _DATE_ORIGIN = _DATE_TIME_ORIGIN.date()
 
 
 @d.dataclass(slots=True, repr=False, eq=False)
-class logger_t(logger_base_t):
+class logger_t(base_t):
     """
     intercepted_wrn_handle: When warning interception is on, this stores the original
         "handle" method of the Python warning logger.
@@ -85,9 +88,9 @@ class logger_t(logger_base_t):
         self, name_: str, level_: int, activate_wrn_interceptions: bool
     ) -> None:
         """"""
-        logger_base_t.__init__(self, name_)
+        base_t.__init__(self, name_)
         self.setLevel(level_)
-        self.propagate = False  # Part of logger_base_t.
+        self.propagate = False  # Part of base_t.
 
         for level in l.getLevelNamesMapping().values():
             self.events[level] = 0
@@ -97,6 +100,39 @@ class logger_t(logger_base_t):
         if self.exit_on_error:
             self.exit_on_critical = True
 
+    def SetLevel(
+        self,
+        level: int,
+        /,
+        *,
+        which: handler_codes_h | str = "a",
+    ) -> None:
+        """
+        Set level of handlers, but the logger level is not modified.
+
+        which: if not a handler_codes_h, then it corresponds to a handler name.
+        """
+        found = False
+        for handler in self.handlers:
+            if (
+                (which == "a")
+                or ((which in "cfg") and (getattr(handler, "kind", None) == which))
+                or (which == handler.name)
+            ):
+                handler.setLevel(level)
+                if which not in HANDLER_KINDS:
+                    return
+                found = True
+
+        if not found:
+            raise ValueError(
+                MessageWithActualExpected(
+                    "Handler not found",
+                    actual=which,
+                    expected=f"{str(HANDLER_KINDS)[1:-1]}, or a handler name",
+                )
+            )
+
     def MakeRich(self, *, alternating_lines: int = 2) -> None:
         """"""
         OverrideExceptionFormat()
@@ -182,10 +218,12 @@ class logger_t(logger_base_t):
 
         return "?", UNKNOWN_MEMORY_USAGE
 
-    def AddHandler(self, handler: l.Handler, should_hold_messages: bool, /) -> None:
+    def AddHandler(
+        self, handler: l.Handler, /, *, should_hold_messages: bool = False
+    ) -> None:
         """"""
         self.should_hold_messages = should_hold_messages
-        logger_base_t.addHandler(self, handler)
+        base_t.addHandler(self, handler)
 
         extension = getattr(handler, "extension", None)
         if extension is None:
@@ -212,7 +250,7 @@ class logger_t(logger_base_t):
 
         if (self.on_hold.__len__() > 0) and not self.should_hold_messages:
             for held in self.on_hold:
-                logger_base_t.handle(self, held)
+                base_t.handle(self, held)
             self.on_hold.clear()
 
         if (date := now.date()) != self.last_message_date:
@@ -229,7 +267,7 @@ class logger_t(logger_base_t):
             if self.should_hold_messages:
                 self.on_hold.append(date_record)
             else:
-                logger_base_t.handle(self, date_record)
+                base_t.handle(self, date_record)
 
         # When.
         if now - self.last_message_now > LONG_ENOUGH:
@@ -271,7 +309,7 @@ class logger_t(logger_base_t):
         if self.should_hold_messages:
             self.on_hold.append(record)
         else:
-            logger_base_t.handle(self, record)
+            base_t.handle(self, record)
 
         if (self.exit_on_critical and (record.levelno is l.CRITICAL)) or (
             self.exit_on_error and (record.levelno is l.ERROR)
@@ -462,10 +500,10 @@ class logger_t(logger_base_t):
         return False
 
 
-def _HandleForWarnings(interceptor: logger_base_t, /) -> logger_handle_h:
+def _HandleForWarnings(interceptor: base_t, /) -> logger_handle_h:
     """"""
 
-    def handle_p(_: logger_base_t, record: l.LogRecord, /) -> None:
+    def handle_p(_: base_t, record: l.LogRecord, /) -> None:
         pieces = WARNING_TYPE_COMPILED_PATTERN.match(record.msg)
         if pieces is None:
             # The warning message does not follow the default format.
@@ -491,11 +529,11 @@ def _HandleForWarnings(interceptor: logger_base_t, /) -> logger_handle_h:
 
 
 def _HandleForInterceptions(
-    intercepted: logger_base_t, interceptor: logger_base_t, /
+    intercepted: base_t, interceptor: base_t, /
 ) -> logger_handle_h:
     """"""
 
-    def handle_p(_: logger_base_t, record: l.LogRecord, /) -> None:
+    def handle_p(_: base_t, record: l.LogRecord, /) -> None:
         duplicate = l.makeLogRecord(record.__dict__)
         duplicate.msg = f"{record.msg} :{intercepted.name}:"
         interceptor.handle(duplicate)
diff --git a/package/logger_36/version.py b/package/logger_36/version.py
index c4211f1..c819449 100644
--- a/package/logger_36/version.py
+++ b/package/logger_36/version.py
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
 SEE COPYRIGHT NOTICE BELOW
 """
 
-__version__ = "2025.5"
+__version__ = "2025.6"
 
 """
 COPYRIGHT NOTICE
diff --git a/test/exception.py b/test/exception.py
index 7db549b..e6bd080 100644
--- a/test/exception.py
+++ b/test/exception.py
@@ -7,7 +7,10 @@ SEE COPYRIGHT NOTICE BELOW
 from logger_36.exception import OverrideExceptionFormat
 
 OverrideExceptionFormat()
-raise ValueError("Invalid value")
+numerator = 1
+denominator = 0
+invalid = numerator / denominator
+# raise ValueError("Invalid value")
 
 
 """
-- 
GitLab