logger-36 2025.21__py3-none-any.whl → 2025.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,11 +8,20 @@ try:
8
8
  import rich # noqa
9
9
  except ModuleNotFoundError:
10
10
  RICH_IS_AVAILABLE = False
11
- from logger_36.constant.error import MISSING_RICH_MESSAGE
11
+ from logger_36.constant.error import MISSING_RICH_MESSAGE # noqa
12
12
  else:
13
13
  RICH_IS_AVAILABLE = True
14
14
  MISSING_RICH_MESSAGE = None
15
15
 
16
+ from logger_36.task.measure.memory import CanCheckUsage as CanCheckMemoryUsage
17
+
18
+ if CanCheckMemoryUsage():
19
+ MEMORY_MEASURE_IS_AVAILABLE = True
20
+ MEMORY_MEASURE_ERROR = None
21
+ else:
22
+ MEMORY_MEASURE_IS_AVAILABLE = False
23
+ from logger_36.constant.error import MEMORY_MEASURE_ERROR # noqa
24
+
16
25
  """
17
26
  COPYRIGHT NOTICE
18
27
 
@@ -29,7 +29,7 @@ class console_handler_t(base_t):
29
29
  """"""
30
30
  s.__stdout__.write(self.MessageFromRecord(record)[0] + "\n")
31
31
 
32
- def EmitAsIs(self, message: str, /) -> None:
32
+ def EmitMessage(self, message: str, /) -> None:
33
33
  """"""
34
34
  s.__stdout__.write(message + "\n")
35
35
 
@@ -96,7 +96,7 @@ class console_rich_handler_t(base_t):
96
96
  """"""
97
97
  return cls(name, message_width, level, kwargs)
98
98
 
99
- def Rule(self, text: str | None, /, *, color: str = "black") -> str | rule_t:
99
+ def Rule(self, /, *, text: str | None = None, color: str = "black") -> str | rule_t:
100
100
  """"""
101
101
  if text is None:
102
102
  return rule_t(style=color)
@@ -116,7 +116,7 @@ class console_rich_handler_t(base_t):
116
116
  self.console.print(message, crop=False, overflow="ignore")
117
117
  self._log_parity = not self._log_parity
118
118
 
119
- def EmitAsIs(self, message: str | renderable_t, /) -> None:
119
+ def EmitMessage(self, message: str | renderable_t, /) -> None:
120
120
  """"""
121
121
  self.console.print(message, crop=False, overflow="ignore")
122
122
 
@@ -32,7 +32,7 @@ class file_handler_t(base_t):
32
32
  self.stream.write(output[0] + "\n")
33
33
  self.stream.flush()
34
34
 
35
- def EmitAsIs(self, message: str, /) -> None:
35
+ def EmitMessage(self, message: str, /) -> None:
36
36
  """"""
37
37
  self.stream.write(message + "\n")
38
38
  self.stream.flush()
@@ -38,10 +38,10 @@ class generic_handler_t(base_t):
38
38
  self, name: str | None, message_width: int, level: int, kwargs
39
39
  ) -> None:
40
40
  """
41
- EmitAsIs: By definition, the generic handler does not know how to output
41
+ EmitMessage: By definition, the generic handler does not know how to output
42
42
  messages. If not passed, it defaults to output-ing messages in the console.
43
43
  """
44
- EmitAsIs = kwargs.pop("EmitAsIs", None)
44
+ EmitMessage = kwargs.pop(base_t.EmitMessage.__name__, None)
45
45
  alternating_logs = kwargs.pop("alternating_logs", 0)
46
46
  supports_html = kwargs.pop("supports_html", False)
47
47
 
@@ -49,8 +49,8 @@ class generic_handler_t(base_t):
49
49
 
50
50
  base_t.__init__(self, name, message_width, None, level, kwargs)
51
51
 
52
- if EmitAsIs is not None:
53
- self.EmitAsIs = EmitAsIs
52
+ if EmitMessage is not None:
53
+ self.EmitMessage = EmitMessage
54
54
  self.is_rich = False
55
55
  self.console = None # console_t | None.
56
56
  self.console_options = None # rich.console.ConsoleOptions | None.
@@ -82,14 +82,14 @@ class generic_handler_t(base_t):
82
82
  """"""
83
83
  return cls(name, message_width, level, kwargs)
84
84
 
85
- def Rule(self, text: str | None, /, *, color: str = "black") -> str | rule_t:
85
+ def Rule(self, /, *, text: str | None = None, color: str = "black") -> str | rule_t:
86
86
  """"""
87
87
  if self.is_rich:
88
88
  if text is None:
89
89
  return rule_t(style=color)
90
90
  return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
91
91
 
92
- return base_t.Rule(self, text, color=color)
92
+ return base_t.Rule(self, text=text, color=color)
93
93
 
94
94
  def emit(self, record: l.LogRecord, /) -> None:
95
95
  """"""
@@ -131,7 +131,7 @@ class generic_handler_t(base_t):
131
131
  else:
132
132
  message = self.MessageFromRecord(record)[0]
133
133
 
134
- self.EmitAsIs(message)
134
+ self.EmitMessage(message)
135
135
  self._log_parity = not self._log_parity
136
136
 
137
137
 
@@ -0,0 +1,98 @@
1
+ """
2
+ Copyright CNRS (https://www.cnrs.fr/index.php/en)
3
+ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
4
+ SEE COPYRIGHT NOTICE BELOW
5
+ """
6
+
7
+ import json
8
+ import logging as l
9
+ import typing as h
10
+
11
+ from logger_36.type.handler import handler_t as base_t
12
+
13
+ formats_h = h.Literal["json", "message", "raw"]
14
+
15
+
16
+ class memory_handler_t(base_t):
17
+ def __init__(
18
+ self, name: str | None, message_width: int, level: int, format_: formats_h
19
+ ) -> None:
20
+ """"""
21
+ assert format_ in h.get_args(formats_h)
22
+
23
+ base_t.__init__(self, name, message_width, None, level)
24
+
25
+ self.format_ = format_
26
+ self.records = []
27
+
28
+ @classmethod
29
+ def New(
30
+ cls,
31
+ /,
32
+ *,
33
+ name: str | None = None,
34
+ message_width: int = -1,
35
+ level: int = l.NOTSET,
36
+ format_: formats_h = "message",
37
+ **_,
38
+ ) -> h.Self:
39
+ """"""
40
+ return cls(name, message_width, level, format_)
41
+
42
+ def emit(self, record: l.LogRecord, /) -> None:
43
+ """"""
44
+ level = record.levelno
45
+
46
+ if self.format_ == "raw":
47
+ is_not_a_rule = True
48
+ elif self.format_ == "message":
49
+ record, is_not_a_rule = self.MessageFromRecord(record)
50
+ else:
51
+ record = json.dumps(record.__dict__)
52
+ is_not_a_rule = True
53
+
54
+ self.records.append((level, record, is_not_a_rule))
55
+
56
+ def EmitMessage(self, message: str, /) -> None:
57
+ """"""
58
+ if self.format_ == "message":
59
+ message_key = "msg"
60
+ else:
61
+ message_key = "message"
62
+ record = l.makeLogRecord(
63
+ {"name": "<UNKNOWN LOGGER>", "levelno": l.INFO, message_key: message}
64
+ )
65
+ self.emit(record)
66
+
67
+
68
+ """
69
+ COPYRIGHT NOTICE
70
+
71
+ This software is governed by the CeCILL license under French law and
72
+ abiding by the rules of distribution of free software. You can use,
73
+ modify and/ or redistribute the software under the terms of the CeCILL
74
+ license as circulated by CEA, CNRS and INRIA at the following URL
75
+ "http://www.cecill.info".
76
+
77
+ As a counterpart to the access to the source code and rights to copy,
78
+ modify and redistribute granted by the license, users are provided only
79
+ with a limited warranty and the software's author, the holder of the
80
+ economic rights, and the successive licensors have only limited
81
+ liability.
82
+
83
+ In this respect, the user's attention is drawn to the risks associated
84
+ with loading, using, modifying and/or developing or reproducing the
85
+ software by the user in light of its specific status of free software,
86
+ that may mean that it is complicated to manipulate, and that also
87
+ therefore means that it is reserved for developers and experienced
88
+ professionals having in-depth computer knowledge. Users are therefore
89
+ encouraged to load and test the software's suitability as regards their
90
+ requirements in conditions enabling the security of their systems and/or
91
+ data to be ensured and, more generally, to use and operate it in the
92
+ same conditions as regards security.
93
+
94
+ The fact that you are presently reading this means that you have had
95
+ knowledge of the CeCILL license and that you accept its terms.
96
+
97
+ SEE LICENCE NOTICE: file README-LICENCE-utf8.txt at project source root.
98
+ """
@@ -4,8 +4,10 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
4
4
  SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
- SHOW_W_RULE_ATTR = "should_show_w_rule"
8
7
  SHOW_WHERE_ATTR = "should_show_where"
8
+ SHOW_W_RULE_ATTR = "should_show_w_rule"
9
+ WHEN_OR_ELAPSED_ATTR = "when_or_elapsed"
10
+ WHERE_ATTR = "where"
9
11
 
10
12
  """
11
13
  COPYRIGHT NOTICE
logger_36/task/storage.py CHANGED
@@ -10,25 +10,28 @@ from pathlib import Path as path_t
10
10
 
11
11
  from logger_36.constant.html import BODY_PLACEHOLDER, MINIMAL_HTML, TITLE_PLACEHOLDER
12
12
  from logger_36.instance.logger import L
13
+ from logger_36.type.logger import logger_t
13
14
 
14
15
 
15
- def SaveLOGasHTML(path: str | path_t | io_base_t | None = None) -> None:
16
+ def SaveLOGasHTML(
17
+ path: str | path_t | io_base_t | None = None, /, *, logger: logger_t = L
18
+ ) -> None:
16
19
  """
17
20
  From first console handler found.
18
21
  """
19
22
  cannot_save = "Cannot save logging record as HTML"
20
23
 
21
24
  if path is None:
22
- for handler in L.handlers:
25
+ for handler in logger.handlers:
23
26
  if isinstance(handler, l.FileHandler):
24
27
  path = path_t(handler.baseFilename).with_suffix(".htm")
25
28
  break
26
29
  else:
27
- L.warning(f"{cannot_save}: No file handler to build a filename from.")
30
+ logger.warning(f"{cannot_save}: No file handler to build a filename from.")
28
31
  return
29
32
 
30
33
  if path.exists():
31
- L.warning(
34
+ logger.warning(
32
35
  f'{cannot_save}: Automatically generated path "{path}" already exists.'
33
36
  )
34
37
  return
@@ -38,13 +41,38 @@ def SaveLOGasHTML(path: str | path_t | io_base_t | None = None) -> None:
38
41
  actual_file = isinstance(path, path_t)
39
42
 
40
43
  if actual_file and path.exists():
41
- L.warning(f'{cannot_save}: File "{path}" already exists.')
44
+ logger.warning(f'{cannot_save}: File "{path}" already exists.')
42
45
  return
43
46
 
44
- body = "\n".join(map(_HighlightedEvent, L.recorded))
45
- html = MINIMAL_HTML.replace(TITLE_PLACEHOLDER, L.name).replace(
47
+ for handler in logger.handlers:
48
+ records = getattr(handler, "records", None)
49
+ if isinstance(records, list) and (
50
+ (records.__len__() == 0)
51
+ or all(
52
+ isinstance(_, tuple)
53
+ and isinstance(_[0], int)
54
+ and isinstance(_[1], dict | str | l.LogRecord)
55
+ and isinstance(_[2], bool)
56
+ for _ in records
57
+ )
58
+ ):
59
+ break
60
+ else:
61
+ logger.warning(f"{cannot_save}: No handlers with recording capability found.")
62
+ return
63
+
64
+ if records.__len__() == 0:
65
+ return
66
+
67
+ if isinstance(records[0][1], str):
68
+ records = map(_HighlightedEvent, records)
69
+ else:
70
+ records = map(lambda _: str(_[1]), records)
71
+ body = "\n".join(records)
72
+ html = MINIMAL_HTML.replace(TITLE_PLACEHOLDER, logger.name).replace(
46
73
  BODY_PLACEHOLDER, body
47
74
  )
75
+
48
76
  if actual_file:
49
77
  with open(path, "w") as accessor:
50
78
  accessor.write(html)
@@ -52,21 +80,25 @@ def SaveLOGasHTML(path: str | path_t | io_base_t | None = None) -> None:
52
80
  path.write(html)
53
81
 
54
82
 
55
- def _HighlightedEvent(event: tuple[int, str], /) -> str:
83
+ def _HighlightedEvent(event: tuple[int, str, bool], /) -> str:
56
84
  """"""
57
- level, message = event
58
- if level == l.DEBUG:
59
- color = "BlueViolet"
60
- elif level == l.INFO:
61
- color = "black"
62
- elif level == l.WARNING:
63
- color = "gold"
64
- elif level == l.ERROR:
65
- color = "orange"
66
- elif level == l.CRITICAL:
67
- color = "red"
85
+ level, message, is_not_a_rule = event
86
+
87
+ if is_not_a_rule:
88
+ if level == l.DEBUG:
89
+ color = "BlueViolet"
90
+ elif level == l.INFO:
91
+ color = "black"
92
+ elif level == l.WARNING:
93
+ color = "gold"
94
+ elif level == l.ERROR:
95
+ color = "orange"
96
+ elif level == l.CRITICAL:
97
+ color = "red"
98
+ else:
99
+ color = "black"
68
100
  else:
69
- color = "black"
101
+ color = "DarkTurquoise"
70
102
 
71
103
  return f'<span style="color:{color}">{message}</span>'
72
104
 
logger_36/type/handler.py CHANGED
@@ -16,7 +16,7 @@ from logger_36.config.message import (
16
16
  WHERE_SEPARATOR,
17
17
  )
18
18
  from logger_36.constant.message import NEXT_LINE_PROLOGUE
19
- from logger_36.constant.record import SHOW_W_RULE_ATTR
19
+ from logger_36.constant.record import SHOW_W_RULE_ATTR, WHEN_OR_ELAPSED_ATTR, WHERE_ATTR
20
20
  from logger_36.constant.rule import (
21
21
  DEFAULT_RULE,
22
22
  DEFAULT_RULE_LENGTH,
@@ -66,7 +66,7 @@ class extension_t:
66
66
  message = self.PreProcessedMessage(message)
67
67
 
68
68
  if hasattr(record, SHOW_W_RULE_ATTR):
69
- return self.Rule(message, color=rule_color), False
69
+ return self.Rule(text=message, color=rule_color), False
70
70
 
71
71
  if (self.message_width <= 0) or (message.__len__() <= self.message_width):
72
72
  if "\n" in message:
@@ -78,11 +78,11 @@ class extension_t:
78
78
  lines = WrappedLines([message], self.message_width)
79
79
  message = NEXT_LINE_PROLOGUE.join(lines)
80
80
 
81
- when_or_elapsed = getattr(record, "when_or_elapsed", None)
81
+ when_or_elapsed = getattr(record, WHEN_OR_ELAPSED_ATTR, None)
82
82
  if when_or_elapsed is None:
83
83
  return message, True
84
84
 
85
- if (where := getattr(record, "where", None)) is None:
85
+ if (where := getattr(record, WHERE_ATTR, None)) is None:
86
86
  where = ""
87
87
  else:
88
88
  where = f"{NEXT_LINE_PROLOGUE}{WHERE_SEPARATOR} {where}"
@@ -93,7 +93,7 @@ class extension_t:
93
93
  f"{MESSAGE_MARKER} {message}{where}"
94
94
  ), True
95
95
 
96
- def Rule(self, text: str | None, /, *, color: str = "black") -> str | h.Any:
96
+ def Rule(self, /, *, text: str | None = None, color: str = "black") -> str | h.Any:
97
97
  """
98
98
  Return type hint h.Any: For Rich, for example.
99
99
  """
@@ -113,13 +113,13 @@ class extension_t:
113
113
 
114
114
  return f"{half_rule} {text} {half_rule}"
115
115
 
116
- def EmitAsIs(self, message: str, /) -> None:
116
+ def EmitMessage(self, message: str, /) -> None:
117
117
  """"""
118
118
  raise NotImplementedError
119
119
 
120
120
  def EmitRule(self, /, *, text: str | None = None, color: str = "black") -> None:
121
121
  """"""
122
- self.EmitAsIs(self.Rule(text, color=color))
122
+ self.EmitMessage(self.Rule(text=text, color=color))
123
123
 
124
124
 
125
125
  class handler_t(l.Handler, extension_t):
logger_36/type/logger.py CHANGED
@@ -19,7 +19,12 @@ from os import sep as FOLDER_SEPARATOR
19
19
  from pathlib import Path as path_t
20
20
  from traceback import TracebackException as traceback_t
21
21
 
22
- from logger_36.catalog.config.optional import MISSING_RICH_MESSAGE, RICH_IS_AVAILABLE
22
+ from logger_36.catalog.config.optional import (
23
+ MEMORY_MEASURE_ERROR,
24
+ MEMORY_MEASURE_IS_AVAILABLE,
25
+ MISSING_RICH_MESSAGE,
26
+ RICH_IS_AVAILABLE,
27
+ )
23
28
  from logger_36.catalog.handler.console import console_handler_t
24
29
  from logger_36.catalog.handler.file import file_handler_t
25
30
  from logger_36.config.issue import ISSUE_CONTEXT_END, ISSUE_CONTEXT_SEPARATOR
@@ -30,7 +35,6 @@ from logger_36.config.message import (
30
35
  TIME_FORMAT,
31
36
  WHERE_SEPARATOR,
32
37
  )
33
- from logger_36.constant.error import MEMORY_MEASURE_ERROR
34
38
  from logger_36.constant.generic import NOT_PASSED
35
39
  from logger_36.constant.issue import ISSUE_LEVEL_SEPARATOR, ORDER, order_h
36
40
  from logger_36.constant.logger import WARNING_LOGGER_NAME, WARNING_TYPE_COMPILED_PATTERN
@@ -40,7 +44,6 @@ from logger_36.constant.path import PROJECT_FILE_RELATIVE, USER_FOLDER
40
44
  from logger_36.constant.record import SHOW_W_RULE_ATTR, SHOW_WHERE_ATTR
41
45
  from logger_36.task.format.message import MessageWithActualExpected
42
46
  from logger_36.task.measure.chronos import ElapsedTime
43
- from logger_36.task.measure.memory import CanCheckUsage as CanCheckMemoryUsage
44
47
  from logger_36.task.measure.memory import CurrentUsage as CurrentMemoryUsage
45
48
  from logger_36.type.handler import any_handler_t as base_handler_t
46
49
  from logger_36.type.handler import extension_t as handler_extension_t
@@ -61,7 +64,6 @@ logger_handle_h = logger_handle_raw_h | logger_handle_with_self_h
61
64
 
62
65
  _DATE_TIME_ORIGIN = date_time_t.fromtimestamp(1970, None)
63
66
  _DATE_ORIGIN = _DATE_TIME_ORIGIN.date()
64
- _MEMORY_MEASURE_ERROR = MEMORY_MEASURE_ERROR
65
67
 
66
68
 
67
69
  @d.dataclass(slots=True, repr=False, eq=False)
@@ -73,20 +75,15 @@ class logger_t(base_t):
73
75
  _should_activate_log_interceptions: Loggers instantiated after a logger_t logger
74
76
  will be missed by an early call of ToggleLogInterceptions. Therefore, passing True
75
77
  for activate_log_interceptions only sets _should_activate_log_interceptions to True,
76
- which is later checked in AddHandler to effectively call ToggleLogInterceptions if
77
- _should_hold_messages is False (which normally indicates that the handler about to
78
- be added is the last one).
79
-
80
- _should_hold_messages: Must not be False until at least one handler has been added.
78
+ which is later checked in AddHandler to effectively call ToggleLogInterceptions.
81
79
  """
82
80
 
83
- should_record_messages: bool = False
84
- should_watch_memory_usage: bool = False
85
81
  exit_on_error: bool = False # Implies exit_on_critical.
86
82
  exit_on_critical: bool = False
83
+ should_monitor_memory_usage: bool = False
87
84
 
88
- events: dict[int, int] = d.field(init=False, default_factory=dict)
89
- recorded: list[tuple[int, str]] = d.field(init=False, default_factory=list)
85
+ history: dict[date_time_t, str] = d.field(init=False, default_factory=dict)
86
+ n_events: dict[int, int] = d.field(init=False, default_factory=dict)
90
87
 
91
88
  last_message_now: date_time_t = d.field(init=False, default=_DATE_TIME_ORIGIN)
92
89
  last_message_date: date_t = d.field(init=False, default=_DATE_ORIGIN)
@@ -98,10 +95,9 @@ class logger_t(base_t):
98
95
  init=False, default_factory=dict
99
96
  )
100
97
  intercepts_exceptions: bool = d.field(init=False, default=False)
101
- _should_hold_messages: bool = d.field(init=False, default=True)
98
+
99
+ # Used only until the last handler is added (see AddHandler).
102
100
  _should_activate_log_interceptions: bool = d.field(init=False, default=False)
103
- _on_hold: list[l.LogRecord] = d.field(init=False, default_factory=list)
104
- _recording_handler: handler_extension_t | None = d.field(init=False, default=None)
105
101
 
106
102
  name_: d.InitVar[str | None] = None
107
103
  level_: d.InitVar[int] = l.NOTSET
@@ -156,8 +152,6 @@ class logger_t(base_t):
156
152
  activate_exc_interceptions: bool,
157
153
  ) -> None:
158
154
  """"""
159
- global _MEMORY_MEASURE_ERROR
160
-
161
155
  if name_ is None:
162
156
  name_ = f"{type(self).__name__}:{hex(id(self))[2:]}"
163
157
 
@@ -169,12 +163,7 @@ class logger_t(base_t):
169
163
  self.exit_on_critical = True
170
164
 
171
165
  for level_id in l.getLevelNamesMapping().values():
172
- self.events[level_id] = 0
173
-
174
- if self.should_record_messages:
175
- self.ActivateMessageRecording()
176
-
177
- self.info(f'New logger "{self.name}" for "{PROJECT_FILE_RELATIVE}"')
166
+ self.n_events[level_id] = 0
178
167
 
179
168
  if activate_wrn_interceptions:
180
169
  self.ToggleWarningInterceptions(True)
@@ -183,11 +172,12 @@ class logger_t(base_t):
183
172
  if activate_exc_interceptions:
184
173
  self.ToggleExceptionInterceptions(True)
185
174
 
186
- if self.should_watch_memory_usage and not CanCheckMemoryUsage():
187
- self.should_watch_memory_usage = False
188
- if _MEMORY_MEASURE_ERROR is not None:
189
- s.__stderr__.write(_MEMORY_MEASURE_ERROR + "\n")
190
- _MEMORY_MEASURE_ERROR = None
175
+ if self.should_monitor_memory_usage:
176
+ self.ActivateMemoryUsageMonitoring()
177
+
178
+ self.history[date_time_t.now()] = (
179
+ f'Logger "{self.name}" instantiation for "{PROJECT_FILE_RELATIVE}"'
180
+ )
191
181
 
192
182
  def handle(self, record: l.LogRecord, /) -> None:
193
183
  """"""
@@ -206,19 +196,8 @@ class logger_t(base_t):
206
196
 
207
197
  # Where.
208
198
  should_show_where = getattr(record, SHOW_WHERE_ATTR, record.levelno != l.INFO)
209
- if should_show_where or self.should_watch_memory_usage:
210
- module = path_t(record.pathname)
211
- for path in s.path:
212
- if module.is_relative_to(path):
213
- module = module.relative_to(path).with_suffix("")
214
- module = str(module).replace(FOLDER_SEPARATOR, ".")
215
- break
216
- else:
217
- if module.is_relative_to(USER_FOLDER):
218
- module = module.relative_to(USER_FOLDER)
219
- where = f"{module}:{record.funcName}:{record.lineno}"
220
- if should_show_where:
221
- record.where = where
199
+ if should_show_where or self.should_monitor_memory_usage:
200
+ where = _RecordLocation(record, should_show_where)
222
201
  else:
223
202
  where = None
224
203
 
@@ -226,13 +205,10 @@ class logger_t(base_t):
226
205
  if not isinstance(record.msg, str):
227
206
  record.msg = str(record.msg)
228
207
 
229
- if self._should_hold_messages:
230
- self._on_hold.append(record)
231
- else:
232
- self._HandleRecord(record)
233
- self.events[record.levelno] += 1
208
+ base_t.handle(self, record)
209
+ self.n_events[record.levelno] += 1
234
210
 
235
- if self.should_watch_memory_usage:
211
+ if self.should_monitor_memory_usage:
236
212
  self.memory_usages.append((where, CurrentMemoryUsage()))
237
213
 
238
214
  if (self.exit_on_critical and (record.levelno is l.CRITICAL)) or (
@@ -242,13 +218,6 @@ class logger_t(base_t):
242
218
  # __post_init__ set self.exit_on_critical if self.exit_on_error.
243
219
  s.exit(1)
244
220
 
245
- def _FlushRecordsOnHold(self) -> None:
246
- """"""
247
- for held in self._on_hold:
248
- self._HandleRecord(held)
249
-
250
- self._on_hold.clear()
251
-
252
221
  def _AcknowledgeDateChange(self, date: date_t, /) -> None:
253
222
  """"""
254
223
  self.last_message_date = date
@@ -261,30 +230,12 @@ class logger_t(base_t):
261
230
  SHOW_W_RULE_ATTR: True,
262
231
  }
263
232
  )
264
-
265
- if self._should_hold_messages:
266
- self._on_hold.append(record)
267
- else:
268
- self._HandleRecord(record)
269
-
270
- def _HandleRecord(self, record: l.LogRecord, /) -> None:
271
- """"""
272
- if self.should_record_messages:
273
- message = self._recording_handler.MessageFromRecord(record)[0]
274
- self.recorded.append((record.levelno, message))
275
-
276
233
  base_t.handle(self, record)
277
234
 
278
- def ActivateMessageRecording(self) -> None:
279
- """"""
280
- self._recording_handler = handler_extension_t("recording_handler", 0, None)
281
- self.should_record_messages = True # Useless if called from __post_init__.
282
- self.info(f'Message recording activated for logger "{self.name}"')
283
-
284
235
  def ResetEventCounts(self) -> None:
285
236
  """"""
286
- for level_id in self.events:
287
- self.events[level_id] = 0
237
+ for level_id in self.n_events:
238
+ self.n_events[level_id] = 0
288
239
 
289
240
  def ToggleWarningInterceptions(self, state: bool, /) -> None:
290
241
  """"""
@@ -297,7 +248,7 @@ class logger_t(base_t):
297
248
  logger.handle = t.MethodType(_HandleForWarnings(self), logger)
298
249
 
299
250
  l.captureWarnings(True)
300
- self.info("Warning Interception: ON")
251
+ self.history[date_time_t.now()] = "Warning Interception: ON"
301
252
  else:
302
253
  if not self.intercepts_warnings:
303
254
  return
@@ -307,12 +258,12 @@ class logger_t(base_t):
307
258
  self.intercepted_wrn_handle = None
308
259
 
309
260
  l.captureWarnings(False)
310
- self.info("Warning Interception: OFF")
261
+ self.history[date_time_t.now()] = "Warning Interception: OFF"
311
262
 
312
263
  def ToggleLogInterceptions(self, state: bool, /) -> None:
313
264
  """"""
314
265
  if state:
315
- if self.intercepts_logs:
266
+ if self._should_activate_log_interceptions or self.intercepts_logs:
316
267
  return
317
268
 
318
269
  # Note: Alternative to self.manager is logging.root.manager.
@@ -331,8 +282,14 @@ class logger_t(base_t):
331
282
  intercepted = sorted(self.intercepted_log_handles.keys())
332
283
  if intercepted.__len__() > 0:
333
284
  as_str = ", ".join(intercepted)
334
- self.info(f"Now Intercepting LOGs from: {as_str}")
285
+ self.history[date_time_t.now()] = (
286
+ f"Now Intercepting LOGs from: {as_str}"
287
+ )
335
288
  else:
289
+ if self._should_activate_log_interceptions:
290
+ self._should_activate_log_interceptions = False
291
+ return
292
+
336
293
  if not self.intercepts_logs:
337
294
  return
338
295
 
@@ -340,7 +297,7 @@ class logger_t(base_t):
340
297
  logger = l.getLogger(name)
341
298
  logger.handle = handle
342
299
  self.intercepted_log_handles.clear()
343
- self.info("Log Interception: OFF")
300
+ self.history[date_time_t.now()] = "Log Interception: OFF"
344
301
 
345
302
  def ToggleExceptionInterceptions(self, state: bool, /) -> None:
346
303
  """"""
@@ -351,7 +308,7 @@ class logger_t(base_t):
351
308
  s.excepthook = self.DealWithException
352
309
  thrd.excepthook = self.DealWithExceptionInThread
353
310
  self.intercepts_exceptions = True
354
- self.info("Exception Interception: ON")
311
+ self.history[date_time_t.now()] = "Exception Interception: ON"
355
312
  else:
356
313
  if not self.intercepts_exceptions:
357
314
  return
@@ -359,7 +316,19 @@ class logger_t(base_t):
359
316
  s.excepthook = s.__excepthook__
360
317
  thrd.excepthook = thrd.__excepthook__
361
318
  self.intercepts_exceptions = False
362
- self.info("Exception Interception: OFF")
319
+ self.history[date_time_t.now()] = "Exception Interception: OFF"
320
+
321
+ def ActivateMemoryUsageMonitoring(self) -> None:
322
+ """"""
323
+ if MEMORY_MEASURE_IS_AVAILABLE:
324
+ # Useless if called from __post_init__.
325
+ self.should_monitor_memory_usage = True
326
+ self.history[date_time_t.now()] = (
327
+ f'Memory usage monitoring activated for logger "{self.name}"'
328
+ )
329
+ else:
330
+ self.should_monitor_memory_usage = False
331
+ self.error(MEMORY_MEASURE_ERROR)
363
332
 
364
333
  def AddHandler(
365
334
  self,
@@ -372,22 +341,16 @@ class logger_t(base_t):
372
341
  name: str | None = None,
373
342
  level: int = l.INFO,
374
343
  message_width: int = -1,
375
- should_still_hold_messages: bool = False,
376
344
  **kwargs,
377
345
  ) -> None:
378
346
  """
379
347
  Silently ignores re-holding request after un-holding.
380
348
  """
381
- should_flush_on_hold = False
382
- new_handler_warning = ""
383
- if self._should_hold_messages and not should_still_hold_messages:
384
- if self._should_activate_log_interceptions:
385
- self.ToggleLogInterceptions(True)
386
- self._should_activate_log_interceptions = False
387
-
388
- self._should_hold_messages = False
389
- should_flush_on_hold = True
390
- new_handler_warning = "\n(Handlers added from now on will miss above logs.)"
349
+ if self._should_activate_log_interceptions:
350
+ # Turn _should_activate_log_interceptions off before calling
351
+ # ToggleLogInterceptions because it checks it.
352
+ self._should_activate_log_interceptions = False
353
+ self.ToggleLogInterceptions(True)
391
354
 
392
355
  if isinstance(handler_t_or_handler, type):
393
356
  handler = handler_t_or_handler.New(
@@ -397,17 +360,12 @@ class logger_t(base_t):
397
360
  handler = handler_t_or_handler
398
361
  base_t.addHandler(self, handler)
399
362
 
400
- # Wait until after the handler has been added to flush messages on hold.
401
- if should_flush_on_hold:
402
- self._FlushRecordsOnHold()
403
-
404
363
  path = getattr(handler, "baseFilename", "")
405
364
  if isinstance(path, path_t) or (path.__len__() > 0):
406
365
  path = f"\nPath: {path}"
407
- self.info(
366
+ self.history[date_time_t.now()] = (
408
367
  f'New handler "{handler.name}" of type "{type(handler).__name__}" and '
409
368
  f"level {handler.level}={l.getLevelName(handler.level)}{path}"
410
- f"{new_handler_warning}"
411
369
  )
412
370
 
413
371
  def MakeMonochrome(self) -> None:
@@ -416,13 +374,12 @@ class logger_t(base_t):
416
374
 
417
375
  def MakeRich(self, *, alternating_logs: int = 0) -> None:
418
376
  """"""
419
- if MISSING_RICH_MESSAGE is not None:
420
- s.__stderr__.write(MISSING_RICH_MESSAGE + "\n")
421
-
422
- if console_rich_handler_t is console_handler_t:
423
- handler_kwargs = {}
424
- else:
377
+ if RICH_IS_AVAILABLE:
425
378
  handler_kwargs = {"alternating_logs": alternating_logs}
379
+ else:
380
+ handler_kwargs = {}
381
+ self.error(MISSING_RICH_MESSAGE)
382
+
426
383
  self.AddHandler(console_rich_handler_t, **handler_kwargs)
427
384
 
428
385
  def MakePermanent(self, path: str | path_t, /) -> None:
@@ -509,8 +466,11 @@ class logger_t(base_t):
509
466
  message = text.indent(message, LINE_INDENT)
510
467
 
511
468
  for handler in self.handlers:
512
- if (EmitAsIs := getattr(handler, "EmitAsIs", None)) is not None:
513
- EmitAsIs(message)
469
+ EmitMessage = getattr(
470
+ handler, handler_extension_t.EmitMessage.__name__, None
471
+ )
472
+ if EmitMessage is not None:
473
+ EmitMessage(message)
514
474
 
515
475
  info_raw = LogAsIs # To follow the convention of the logging methods info, error...
516
476
 
@@ -519,7 +479,8 @@ class logger_t(base_t):
519
479
  ) -> None:
520
480
  """"""
521
481
  for handler in self.handlers:
522
- if (EmitRule := getattr(handler, "EmitRule", None)) is not None:
482
+ EmitRule = getattr(handler, handler_extension_t.EmitRule.__name__, None)
483
+ if EmitRule is not None:
523
484
  EmitRule(text=message, color=color)
524
485
 
525
486
  def AddContextLevel(self, new_level: str, /) -> None:
@@ -647,6 +608,26 @@ class logger_t(base_t):
647
608
  return False
648
609
 
649
610
 
611
+ def _RecordLocation(record: l.LogRecord, should_also_store: bool, /) -> str:
612
+ """"""
613
+ module = path_t(record.pathname)
614
+ for path in s.path:
615
+ if module.is_relative_to(path):
616
+ module = module.relative_to(path).with_suffix("")
617
+ module = str(module).replace(FOLDER_SEPARATOR, ".")
618
+ break
619
+ else:
620
+ if module.is_relative_to(USER_FOLDER):
621
+ module = module.relative_to(USER_FOLDER)
622
+
623
+ output = f"{module}:{record.funcName}:{record.lineno}"
624
+
625
+ if should_also_store:
626
+ record.where = output
627
+
628
+ return output
629
+
630
+
650
631
  def _HandleForWarnings(interceptor: base_t, /) -> logger_handle_h:
651
632
  """"""
652
633
 
logger_36/version.py CHANGED
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
4
4
  SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
- __version__ = "2025.21"
7
+ __version__ = "2025.23"
8
8
 
9
9
  """
10
10
  COPYRIGHT NOTICE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logger-36
3
- Version: 2025.21
3
+ Version: 2025.23
4
4
  Summary: Simple logger with a catalog of handlers
5
5
  Home-page: https://src.koda.cnrs.fr/eric.debreuve/logger-36/
6
6
  Author: Eric Debreuve
@@ -1,5 +1,5 @@
1
1
  logger_36/__init__.py,sha256=mK6AD0eWI2Sk42oxleTvsxzYJ28FbHK5WNkpLgAhnNE,2129
2
- logger_36/version.py,sha256=uq-1uJ3Ttvm7hNAwg0S3T103Mh6UIamgGGIWFao8YLI,1680
2
+ logger_36/version.py,sha256=3y8_8Te0bVUofCcWg9yoGDEx2HlTct92Ol2Pk3NTA8w,1680
3
3
  logger_36/api/content.py,sha256=DuT4UX4r_1DTXzuuRD-tvsTZk5X-Nj11loBKhuWOMw0,1791
4
4
  logger_36/api/gpu.py,sha256=NNs1IvQ7bh8Dppm8O8K2YLWbm4rogc3Ie_-D6xzkX3g,1726
5
5
  logger_36/api/memory.py,sha256=vOY4cTTrC3u7L0OrKXdPNlsCahYjCrY4h7iqpGZv9kU,2217
@@ -8,11 +8,12 @@ logger_36/api/system.py,sha256=h-3GfhZPwawv0UKBWKkT1LzxSCZwpA2VIsy03lLYi6w,1725
8
8
  logger_36/api/time.py,sha256=JG0vgzPSRZ7UWQyoihnVu4sjPC-okFIKA3ZyNh2GaZo,1798
9
9
  logger_36/api/type.py,sha256=eLZ2yuH-sYeh4Z2KnAwTRJEbmkmgzBPMncdqXfFUTG8,1760
10
10
  logger_36/catalog/config/console_rich.py,sha256=t9p9-AkSgPiLAsm1evAdbz77g7JcVLePhUJ1FzNi3cY,2330
11
- logger_36/catalog/config/optional.py,sha256=5vabOlEQIFxoT_y4AjP19rpOjBuUJplpuBkLoCIKImA,1872
12
- logger_36/catalog/handler/console.py,sha256=OKtRG3PseT06_UwZBtbragNIKvCqa7ZsLphYEsgeq2c,2308
13
- logger_36/catalog/handler/console_rich.py,sha256=b3B_pQefv0IeQFEj-tZ5-tH8RrcessZI8sI4Tyf17bg,6180
14
- logger_36/catalog/handler/file.py,sha256=JReN8KruN8eKhBfG9sj72kkHfukHK4-8mdSxNpgFhLU,2455
15
- logger_36/catalog/handler/generic.py,sha256=0Z4Vm-AAc92szghYEzihkm9CBRzCvqgdUTXH_p98S4o,6314
11
+ logger_36/catalog/config/optional.py,sha256=8d8HdpE07gHfsdoL8mVAlRlh9AgLcb4z7I7h6ob7CfU,2174
12
+ logger_36/catalog/handler/console.py,sha256=YEQN8fw7ra9w-5sJACNC4pktv9einm5ASP9TncU8TQo,2311
13
+ logger_36/catalog/handler/console_rich.py,sha256=gPpq8ij1ZFTXaLgISaCTXDDwv7uWPivLeq2ttpiuKlk,6190
14
+ logger_36/catalog/handler/file.py,sha256=yg8GnsV6AmDsx1R5iZlsK-5idqD733gc09Syl02TG7Y,2458
15
+ logger_36/catalog/handler/generic.py,sha256=L8mLc-iQlBWKBqEN3-wIKy2yk_TVgqIWYn_P1hXDsQA,6361
16
+ logger_36/catalog/handler/memory.py,sha256=-31hTXgGImC6if4wJF6eegvwYhsYAQsDTkdoXIUxZB8,3224
16
17
  logger_36/catalog/logger/chronos.py,sha256=S4m9TMPQy_Ju500mpE1jNzu2gZG-QKdVuvX9RVRKHR8,1911
17
18
  logger_36/catalog/logger/gpu.py,sha256=Py5YY0nD_pqJzJsEKQYoOGHcPqyNVJ3J2noOS3hDL6g,2890
18
19
  logger_36/catalog/logger/memory.py,sha256=J0ZGKO7j1FZA_aDGxpABtvzDy1RjCDiDmWYh4U98fEI,4253
@@ -29,7 +30,7 @@ logger_36/constant/logger.py,sha256=ZQYX9JiPsoivwRgYNtdEqRKCagSKD88lRqvxP8MX1ZE,
29
30
  logger_36/constant/memory.py,sha256=Q_E5tTWa-cGaNwrE_xmKa3BxQG6oJO6DHczrxc_M4sE,1817
30
31
  logger_36/constant/message.py,sha256=YJOEzdI0ZjUOdHo3CsiS56FVPhrfNoQYvXuUkprH61g,2312
31
32
  logger_36/constant/path.py,sha256=OfLh70Jyc8po9Ls34nQh_bRr3PXyQ3kF9ciR9QPhiqI,2213
32
- logger_36/constant/record.py,sha256=gQCGLxq8Vs789Ty_qaRNKy18mqlyMT_4kyN-T9r_rGE,1734
33
+ logger_36/constant/record.py,sha256=VMqGxVTbtHpRiDOsYiXEoyYOocYFm8vtcRY-7zXzJrk,1796
33
34
  logger_36/constant/rule.py,sha256=tBKQgPTt6G_p5eInDdWoEEAvQFz4WMSt5THsS5jvk14,1779
34
35
  logger_36/constant/system.py,sha256=pLlLXG5sepQlSUOo3TphaGrHg8xzJBp-GxpL2NPP47k,1904
35
36
  logger_36/extension/html_.py,sha256=W9SyiYsaaYHUrHLGAAN2wiJGXUlwOBJ5gzdjmEcnF18,3342
@@ -37,16 +38,16 @@ logger_36/extension/line.py,sha256=joeojQX1bZJM53333mOEU3s-YC5ExGOrN9Cu9Lh5-FU,2
37
38
  logger_36/instance/logger.py,sha256=X_U10RYU1h2Aa70D8hBnmFyJZtRILK16KN-GB4xkHMU,1782
38
39
  logger_36/instance/loggers.py,sha256=inBk4KKrQ-z3szaopQ29-qQwh1iSc842sWo5J6zJoiM,1725
39
40
  logger_36/task/inspection.py,sha256=ZgPcrPo3h_kEnM-UpHDLg7PAFfB2fqsLFdfmi6hlPVA,4484
40
- logger_36/task/storage.py,sha256=KAILmJlF5IULxEX9QRCyXCwcalp5mpunWVh1oXuLvSs,3516
41
+ logger_36/task/storage.py,sha256=a50M628062iGEymuj5d9iKo7Ad01OXCZrEQnfNqy0gY,4486
41
42
  logger_36/task/format/memory.py,sha256=J1Oy3jw8wjSp2kuiRUm_VFpzXOHX2FOc7nuRrCyrskw,3723
42
43
  logger_36/task/format/message.py,sha256=Rm6zymVEEGcgKfmxMPXP7q3PtwZJKlXGhqZ5tnvlwxA,3502
43
44
  logger_36/task/measure/chronos.py,sha256=7ijMZgP4EP18HbLV2yCxpNpRS9724Wyk523f-nkbhUM,2529
44
45
  logger_36/task/measure/memory.py,sha256=kkPHEIUTUhkCOLrAt01eLJLnsnkl0nFPNhFZdIB_JAw,1991
45
- logger_36/type/handler.py,sha256=ChxP1j9PXLmoiNcsOdxI4bYVdr75v7HeCWp_iYJ2WNY,6602
46
+ logger_36/type/handler.py,sha256=4LCFZzI4OEH6lVBrBQ09ZIvF0nq-_j7OOVnujBDyDWA,6665
46
47
  logger_36/type/issue.py,sha256=QHAYf7QgrjJUtF2D46z6X630qTgeP_0FE5hIwf54RsE,2688
47
- logger_36/type/logger.py,sha256=-cEc56RRpYkXaoDjI7PF7q14Mamdc4zu3DmQ-lRjIn8,25823
48
+ logger_36/type/logger.py,sha256=nh7EuJvdkaDrtdHPWIjot2a2hGfAzwufMWm9_2KoDXM,24645
48
49
  logger_36/type/loggers.py,sha256=7EX7Sg_RlduBjdfFlNZmUfNeDloH1xU30Rdkg_-rXh8,3172
49
- logger_36-2025.21.dist-info/METADATA,sha256=bTX5e3Xn8lko_GnzpFd7XnE60aNUv6V_9JyoLwzE2G4,6529
50
- logger_36-2025.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- logger_36-2025.21.dist-info/top_level.txt,sha256=sM95BTMWmslEEgR_1pzwZsOeSp8C_QBiu8ImbFr0XLc,10
52
- logger_36-2025.21.dist-info/RECORD,,
50
+ logger_36-2025.23.dist-info/METADATA,sha256=d94bUT9zjDr1Yjny2bsSobExZ1W0MXcIv2hX_DLLGC8,6529
51
+ logger_36-2025.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
52
+ logger_36-2025.23.dist-info/top_level.txt,sha256=sM95BTMWmslEEgR_1pzwZsOeSp8C_QBiu8ImbFr0XLc,10
53
+ logger_36-2025.23.dist-info/RECORD,,