From 65efe8f0a63bfe273ec8cdb7489c853cc75d85aa Mon Sep 17 00:00:00 2001
From: DEBREUVE Eric <eric.debreuve@cnrs.fr>
Date: Thu, 19 Sep 2024 15:21:49 +0200
Subject: [PATCH] logger now stores event counts, added DisplayRule, rich
 console interprets markup

---
 .../logger_36/catalog/handler/console_rich.py |  9 +++--
 package/logger_36/task/format/message.py      | 11 +----
 package/logger_36/task/format/rule.py         | 14 +++++--
 package/logger_36/type/logger.py              | 40 +++++++++++++------
 package/logger_36/version.py                  |  2 +-
 test/main.py                                  |  2 +
 6 files changed, 47 insertions(+), 31 deletions(-)

diff --git a/package/logger_36/catalog/handler/console_rich.py b/package/logger_36/catalog/handler/console_rich.py
index 6d2aa5e..1091e6e 100644
--- a/package/logger_36/catalog/handler/console_rich.py
+++ b/package/logger_36/catalog/handler/console_rich.py
@@ -31,7 +31,6 @@ from logger_36.task.format.rule import Rule
 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 EscapedForRich
 from rich.text import Text as text_t
 from rich.traceback import install as InstallTracebackHandler
 
@@ -140,7 +139,7 @@ class console_rich_handler_t(lggg.Handler):
         if hasattr(record, SHOW_W_RULE_ATTR):
             richer = Rule(record.msg, DATE_TIME_COLOR)
         else:
-            first, next_s = self.FormattedLines(record, PreProcessed=EscapedForRich)
+            first, next_s = self.FormattedLines(record)
             should_highlight_back = self.alternating_lines == 1
             if self.alternating_lines >= 0:
                 self.alternating_lines = (self.alternating_lines + 1) % 2
@@ -173,7 +172,9 @@ def HighlightedVersion(
     background_is_light: bool = True,
 ) -> renderable_t:
     """"""
-    output = text_t(first_line)
+    # TODO: Is there a way to use html or something to enable message styling,
+    #     regardless of the handler (would require styling conversion; html->rich here).
+    output = text_t.from_markup(first_line)
 
     # Used instead of _CONTEXT_LENGTH which might include \t, thus creating a
     # mismatch between character length and length when displayed in console.
@@ -193,7 +194,7 @@ def HighlightedVersion(
     output.stylize(ELAPSED_TIME_COLOR, start=elapsed_time_separator)
 
     if next_lines is not None:
-        output.append(next_lines)
+        output.append(text_t.from_markup(next_lines))
 
     _ = output.highlight_regex(ACTUAL_PATTERNS, style=ACTUAL_COLOR)
     _ = output.highlight_regex(EXPECTED_PATTERNS, style=EXPECTED_COLOR)
diff --git a/package/logger_36/task/format/message.py b/package/logger_36/task/format/message.py
index d9e1f00..cff6f75 100644
--- a/package/logger_36/task/format/message.py
+++ b/package/logger_36/task/format/message.py
@@ -15,7 +15,7 @@ from logger_36.config.message import (
     MESSAGE_MARKER,
 )
 from logger_36.constant.generic import NOT_PASSED
-from logger_36.constant.message import EXPECTED_OP, expected_op_h
+from logger_36.constant.message import expected_op_h
 
 
 def MessageFormat(with_where: bool, with_memory_usage: bool, /) -> str:
@@ -46,15 +46,6 @@ def FormattedMessage(
     with_final_dot: bool = True,
 ) -> str:
     """"""
-    if expected_op not in EXPECTED_OP:
-        raise ValueError(
-            FormattedMessage(
-                'Invalid "expected" section operator',
-                actual=expected_op,
-                expected=f"One of {str(EXPECTED_OP)[1:-1]}",
-            )
-        )
-
     if actual is NOT_PASSED:
         if with_final_dot:
             if message[-1] != ".":
diff --git a/package/logger_36/task/format/rule.py b/package/logger_36/task/format/rule.py
index f64ced3..74c65ec 100644
--- a/package/logger_36/task/format/rule.py
+++ b/package/logger_36/task/format/rule.py
@@ -5,18 +5,24 @@ SEE COPYRIGHT NOTICE BELOW
 """
 
 
-def RuleAsText(text: str, /) -> str:
+def RuleAsText(text: str | None, /) -> str:
     """"""
-    return f"---- ---- ---- ---- {text} ---- ---- ---- ----"
+    if text is None:
+        return "---- ---- ---- ---- ---- ---- ---- ---- ----"
+    else:
+        return f"---- ---- ---- ---- {text} ---- ---- ---- ----"
 
 
 try:
     from rich.rule import Rule as rule_t
     from rich.text import Text as text_t
 
-    def Rule(text: str, color: str, /) -> rule_t:
+    def Rule(text: str | None, color: str, /) -> rule_t | str:
         """"""
-        return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
+        if text is None:
+            return rule_t(style=color)
+        else:
+            return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
 
 except ModuleNotFoundError:
     Rule = lambda _txt, _: RuleAsText(_txt)
diff --git a/package/logger_36/type/logger.py b/package/logger_36/type/logger.py
index c100c24..246db01 100644
--- a/package/logger_36/type/logger.py
+++ b/package/logger_36/type/logger.py
@@ -32,6 +32,7 @@ from logger_36.task.format.memory import (
     FormattedUsageWithAutoUnit as FormattedMemoryUsage,
 )
 from logger_36.task.format.message import FormattedMessage
+from logger_36.task.format.rule import Rule
 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
@@ -42,12 +43,14 @@ class logger_t(lggg.Logger):
     name_: d.InitVar[str] = LOGGER_NAME
     level_: d.InitVar[int] = lggg.NOTSET
     activate_wrn_interceptions: d.InitVar[bool] = True
-    exit_on_error: bool = False  # Implies exit_on_critical.
-    exit_on_critical: bool = False
+
     # Must not be False until at least one handler has been added.
     should_hold_messages: bool = True
+    exit_on_error: bool = False  # Implies exit_on_critical.
+    exit_on_critical: bool = False
 
     on_hold: list[lggg.LogRecord] = d.field(init=False, default_factory=list)
+    events: dict[int, int] = d.field(init=False, default_factory=dict)
     last_message_date: str = d.field(init=False, default="")
     any_handler_shows_memory: bool = d.field(init=False, default=False)
     memory_usages: list[tuple[str, int]] = d.field(init=False, default_factory=list)
@@ -66,11 +69,19 @@ class logger_t(lggg.Logger):
         self.setLevel(level_)
         self.propagate = False  # Part of lggg.Logger.
 
+        for level in lggg.getLevelNamesMapping().values():
+            self.events[level] = 0
+
         if activate_wrn_interceptions:
             self._ActivateWarningInterceptions()
         if self.exit_on_error:
             self.exit_on_critical = True
 
+    def ResetEventCounts(self) -> None:
+        """"""
+        for level in self.events:
+            self.events[level] = 0
+
     def _ActivateWarningInterceptions(self) -> None:
         """
         The log message will not appear if called from __post_init__ since there are no
@@ -214,6 +225,7 @@ class logger_t(lggg.Logger):
         else:
             lggg.Logger.handle(self, record)
 
+        self.events[record.levelno] += 1
         if (self.exit_on_critical and (record.levelno is lggg.CRITICAL)) or (
             self.exit_on_error and (record.levelno is lggg.ERROR)
         ):
@@ -264,6 +276,20 @@ class logger_t(lggg.Logger):
             message = f"{type(exception).__name__}:\n{formatted}"
         self.log(level, message)
 
+    def ShowMessage(self, message: str, /) -> None:
+        """
+        See documentation of
+        logger_36.catalog.handler.generic.generic_handler_t.ShowMessage.
+        """
+        for handler in self.handlers:
+            ShowMessage = getattr(handler, "ShowMessage", None)
+            if ShowMessage is not None:
+                ShowMessage(message)
+
+    def DisplayRule(self, /, *, text: str | None = None, color: str = "white") -> None:
+        """"""
+        self.ShowMessage(Rule(text, color))
+
     def AddContextLevel(self, new_level: str, /) -> None:
         """"""
         self.context_levels.append(new_level)
@@ -360,16 +386,6 @@ class logger_t(lggg.Logger):
                 self.log(int(level), issue, stacklevel=2)
         self.staged_issues.clear()
 
-    def ShowMessage(self, message: str, /) -> None:
-        """
-        See documentation of
-        logger_36.catalog.handler.generic.generic_handler_t.ShowMessage.
-        """
-        for handler in self.handlers:
-            ShowMessage = getattr(handler, "ShowMessage", None)
-            if ShowMessage is not None:
-                ShowMessage(message)
-
     def __enter__(self) -> None:
         """"""
         pass
diff --git a/package/logger_36/version.py b/package/logger_36/version.py
index 7e32636..c4a3382 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__ = "2024.23"
+__version__ = "2024.24"
 
 """
 COPYRIGHT NOTICE
diff --git a/test/main.py b/test/main.py
index 96d8c2f..98d7636 100644
--- a/test/main.py
+++ b/test/main.py
@@ -61,6 +61,8 @@ with TemporaryDirectory() as tmp_folder:
 
     LogSystemDetails()
 
+    LOGGER.DisplayRule()
+
     for level in ("debug", "info", "warning", "error", "critical"):
         LogMessage = getattr(LOGGER, level)
         LogMessage(f"{level.capitalize()} message")
-- 
GitLab