logger-36 2025.24__py3-none-any.whl → 2025.26__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.
logger_36/task/storage.py CHANGED
@@ -5,12 +5,17 @@ SEE COPYRIGHT NOTICE BELOW
5
5
  """
6
6
 
7
7
  import logging as l
8
- import os as o
9
- import tempfile as tmps
10
8
  from io import IOBase as io_base_t
11
9
  from pathlib import Path as path_t
12
10
 
13
- from logger_36.constant.html import BODY_PLACEHOLDER, MINIMAL_HTML, TITLE_PLACEHOLDER
11
+ from logger_36.constant.error import CANNOT_SAVE_RECORDS
12
+ from logger_36.constant.html import (
13
+ BODY_PLACEHOLDER,
14
+ HTML_SUFFIX,
15
+ MINIMAL_HTML,
16
+ TITLE_PLACEHOLDER,
17
+ )
18
+ from logger_36.extension.file import NewTemporaryFile
14
19
  from logger_36.instance.logger import L
15
20
  from logger_36.type.logger import logger_t
16
21
 
@@ -21,60 +26,17 @@ def SaveLOGasHTML(
21
26
  """
22
27
  From first console handler found.
23
28
  """
24
- cannot_save = "Cannot save logging record as HTML"
25
-
26
- if path is None:
27
- for handler in logger.handlers:
28
- if isinstance(handler, l.FileHandler):
29
- path = path_t(handler.baseFilename).with_suffix(".htm")
30
- break
31
- else:
32
- logger.warning(f"{cannot_save}: No file handler to build a filename from.")
33
- return
34
-
35
- if path.exists():
36
- logger.warning(
37
- f'{cannot_save}: Automatically generated path "{path}" already exists.'
38
- )
39
- return
40
- elif isinstance(path, str):
41
- path = path_t(path)
42
-
43
- actual_file = isinstance(path, path_t)
44
-
45
- if actual_file and path.exists():
46
- existing = path
47
-
48
- accessor, path = tmps.mkstemp(suffix=".htm")
49
- o.close(accessor)
50
- path = path_t(path)
51
-
29
+ records = logger_t.Records(logger)
30
+ if records is None:
52
31
  logger.warning(
53
- f'File "{existing}" already exists: Saving LOG as HTML in {path} instead.'
32
+ f"{CANNOT_SAVE_RECORDS}: No handlers with recording capability found"
54
33
  )
55
-
56
- for handler in logger.handlers:
57
- records = getattr(handler, "records", None)
58
- if isinstance(records, list) and (
59
- (records.__len__() == 0)
60
- or all(
61
- isinstance(_, tuple)
62
- and isinstance(_[0], int)
63
- and isinstance(_[1], dict | str | l.LogRecord)
64
- and isinstance(_[2], bool)
65
- for _ in records
66
- )
67
- ):
68
- break
69
- else:
70
- logger.warning(f"{cannot_save}: No handlers with recording capability found.")
71
34
  return
72
-
73
35
  if records.__len__() == 0:
74
36
  return
75
37
 
76
38
  if isinstance(records[0][1], str):
77
- records = map(_HighlightedEvent, records)
39
+ records = map(_HighlightedRecord, records)
78
40
  else:
79
41
  records = map(lambda _: str(_[1]), records)
80
42
  body = "\n".join(records)
@@ -82,16 +44,29 @@ def SaveLOGasHTML(
82
44
  BODY_PLACEHOLDER, body
83
45
  )
84
46
 
85
- if actual_file:
86
- with open(path, "w") as accessor:
87
- accessor.write(html)
47
+ if path is None:
48
+ path = logger_t.StoragePath(logger, HTML_SUFFIX)
49
+ logger.info(f'Saving LOG as HTML in "{path}"')
50
+ elif isinstance(path, str):
51
+ path = path_t(path)
52
+ if path.exists():
53
+ existing = path
54
+ path = NewTemporaryFile(HTML_SUFFIX)
55
+ logger.warning(
56
+ f'File "{existing}" already exists: '
57
+ f'Saving LOG as HTML in "{path}" instead'
58
+ )
88
59
  else:
89
60
  path.write(html)
61
+ return
62
+
63
+ with open(path, "w") as accessor:
64
+ accessor.write(html)
90
65
 
91
66
 
92
- def _HighlightedEvent(event: tuple[int, str, bool], /) -> str:
67
+ def _HighlightedRecord(record: tuple[int, str, bool], /) -> str:
93
68
  """"""
94
- level, message, is_not_a_rule = event
69
+ level, message, is_not_a_rule = record
95
70
 
96
71
  if is_not_a_rule:
97
72
  if level == l.DEBUG:
logger_36/type/handler.py CHANGED
@@ -8,15 +8,14 @@ import logging as l
8
8
  import typing as h
9
9
  from pathlib import Path as path_t
10
10
 
11
- from logger_36.config.message import (
12
- FALLBACK_MESSAGE_WIDTH,
13
- LEVEL_CLOSING,
14
- LEVEL_OPENING,
15
- MESSAGE_MARKER,
16
- WHERE_SEPARATOR,
17
- )
11
+ from logger_36.config.message import FALLBACK_MESSAGE_WIDTH
18
12
  from logger_36.config.rule import DEFAULT_RULE_LENGTH, RULE_CHARACTER
19
- from logger_36.constant.message import NEXT_LINE_PROLOGUE
13
+ from logger_36.constant.message import (
14
+ NEXT_LINE_PROLOGUE,
15
+ TIME_PLACEHOLDER,
16
+ WHERE_PROLOGUE,
17
+ CONTEXT_LENGTH_p_1,
18
+ )
20
19
  from logger_36.constant.record import SHOW_W_RULE_ATTR, WHEN_OR_ELAPSED_ATTR, WHERE_ATTR
21
20
  from logger_36.constant.rule import DEFAULT_RULE, MIN_HALF_RULE_LENGTH
22
21
  from logger_36.extension.line import WrappedLines
@@ -53,47 +52,46 @@ class extension_t:
53
52
 
54
53
  def MessageFromRecord(
55
54
  self, record: l.LogRecord, /, *, rule_color: str = "black"
56
- ) -> tuple[str, bool]:
55
+ ) -> tuple[str, bool, int | None]:
57
56
  """
58
- The second returned value is is_not_a_rule.
57
+ Arguments from second on: is_not_a_rule, where_location.
59
58
  """
60
59
  message = record.msg # See logger_36.catalog.handler.README.txt.
61
60
  if self.PreProcessedMessage is not None:
62
61
  message = self.PreProcessedMessage(message)
63
62
 
64
63
  if hasattr(record, SHOW_W_RULE_ATTR):
65
- return self.Rule(text=message, color=rule_color), False
64
+ return self.Rule(text=message, color=rule_color), False, None
66
65
 
67
66
  if (self.message_width <= 0) or (message.__len__() <= self.message_width):
68
67
  if "\n" in message:
69
68
  message = NEXT_LINE_PROLOGUE.join(message.splitlines())
70
69
  else:
71
70
  if "\n" in message:
72
- lines = WrappedLines(message.splitlines(), self.message_width)
71
+ lines = message.splitlines()
73
72
  else:
74
- lines = WrappedLines([message], self.message_width)
75
- message = NEXT_LINE_PROLOGUE.join(lines)
76
-
77
- when_or_elapsed = getattr(record, WHEN_OR_ELAPSED_ATTR, None)
78
- if when_or_elapsed is None:
79
- return message, True
73
+ lines = [message]
74
+ message = NEXT_LINE_PROLOGUE.join(WrappedLines(lines, self.message_width))
80
75
 
76
+ when_or_elapsed = getattr(record, WHEN_OR_ELAPSED_ATTR, TIME_PLACEHOLDER)
81
77
  if (where := getattr(record, WHERE_ATTR, None)) is None:
78
+ where_location = None
82
79
  where = ""
83
80
  else:
84
- where = f"{NEXT_LINE_PROLOGUE}{WHERE_SEPARATOR} {where}"
81
+ where_location = CONTEXT_LENGTH_p_1 + message.__len__()
82
+ where = f"{WHERE_PROLOGUE}{where}"
85
83
 
86
84
  return (
87
- f"{when_or_elapsed}"
88
- f"{LEVEL_OPENING}{record.levelname[0]}{LEVEL_CLOSING} "
89
- f"{MESSAGE_MARKER} {message}{where}"
90
- ), True
85
+ f"{when_or_elapsed}_{record.levelname[0].lower()} {message}{where}",
86
+ True,
87
+ where_location,
88
+ )
91
89
 
92
90
  def Rule(self, /, *, text: str | None = None, color: str = "black") -> str | h.Any:
93
91
  """
94
92
  Return type hint h.Any: For Rich, for example.
95
93
  """
96
- if text is None:
94
+ if text in (None, ""):
97
95
  if self.message_width > 0:
98
96
  return self.message_width * RULE_CHARACTER
99
97
  return DEFAULT_RULE
logger_36/type/issue.py CHANGED
@@ -8,9 +8,9 @@ import logging as l
8
8
  import typing as h
9
9
 
10
10
  from logger_36.config.issue import ISSUE_BASE_CONTEXT
11
- from logger_36.constant.generic import NOT_PASSED
12
11
  from logger_36.constant.issue import ISSUE_LEVEL_SEPARATOR
13
12
  from logger_36.constant.message import expected_op_h
13
+ from logger_36.extension.sentinel import NOT_PASSED
14
14
  from logger_36.task.format.message import MessageWithActualExpected
15
15
 
16
16
  issue_t = str
@@ -28,11 +28,11 @@ def NewIssue(
28
28
  expected_is_choices: bool = False,
29
29
  expected_op: expected_op_h = "=",
30
30
  with_final_dot: bool = True,
31
- ) -> issue_t:
31
+ ) -> tuple[issue_t, bool]:
32
32
  """"""
33
33
  if context.__len__() == 0:
34
34
  context = ISSUE_BASE_CONTEXT
35
- message = MessageWithActualExpected(
35
+ message, has_actual_expected = MessageWithActualExpected(
36
36
  message,
37
37
  actual=actual,
38
38
  expected=expected,
@@ -41,7 +41,10 @@ def NewIssue(
41
41
  with_final_dot=with_final_dot,
42
42
  )
43
43
 
44
- return f"{level}{ISSUE_LEVEL_SEPARATOR}{context}{separator}{message}"
44
+ return (
45
+ f"{level}{ISSUE_LEVEL_SEPARATOR}{context}{separator}{message}",
46
+ has_actual_expected,
47
+ )
45
48
 
46
49
 
47
50
  """
logger_36/type/logger.py CHANGED
@@ -7,6 +7,7 @@ SEE COPYRIGHT NOTICE BELOW
7
7
  import dataclasses as d
8
8
  import inspect as e
9
9
  import logging as l
10
+ import multiprocessing as prll
10
11
  import sys as s
11
12
  import textwrap as text
12
13
  import threading as thrd
@@ -15,6 +16,8 @@ import types as t
15
16
  import typing as h
16
17
  from datetime import date as date_t
17
18
  from datetime import datetime as date_time_t
19
+ from logging.handlers import QueueHandler as queue_handler_t
20
+ from logging.handlers import QueueListener as log_server_t
18
21
  from pathlib import Path as path_t
19
22
  from traceback import TracebackException as traceback_t
20
23
 
@@ -26,25 +29,32 @@ from logger_36.catalog.config.optional import (
26
29
  )
27
30
  from logger_36.catalog.handler.console import console_handler_t
28
31
  from logger_36.catalog.handler.file import file_handler_t
32
+ from logger_36.catalog.handler.memory import memory_handler_t, records_h
29
33
  from logger_36.config.issue import ISSUE_CONTEXT_END, ISSUE_CONTEXT_SEPARATOR
30
34
  from logger_36.config.message import (
31
35
  DATE_FORMAT,
32
- ELAPSED_TIME_SEPARATOR,
33
36
  LONG_ENOUGH,
34
37
  TIME_FORMAT,
35
38
  WHERE_SEPARATOR,
36
39
  )
37
- from logger_36.constant.date_time import DATE_ORIGIN, DATE_TIME_ORIGIN
38
- from logger_36.constant.generic import NOT_PASSED
40
+ from logger_36.constant.chronos import DATE_ORIGIN, DATE_TIME_ORIGIN
39
41
  from logger_36.constant.issue import ISSUE_LEVEL_SEPARATOR, ORDER, order_h
40
42
  from logger_36.constant.logger import WARNING_LOGGER_NAME, WARNING_TYPE_COMPILED_PATTERN
41
43
  from logger_36.constant.memory import UNKNOWN_MEMORY_USAGE
42
- from logger_36.constant.message import LINE_INDENT, TIME_LENGTH_m_1, expected_op_h
44
+ from logger_36.constant.message import LINE_INDENT, expected_op_h
43
45
  from logger_36.constant.path import USER_FOLDER, LAUNCH_ROOT_FILE_relative
44
- from logger_36.constant.record import SHOW_W_RULE_ATTR, SHOW_WHERE_ATTR
45
- from logger_36.extension.record import RecordLocation
46
+ from logger_36.constant.record import (
47
+ HAS_ACTUAL_EXPECTED_ATTR,
48
+ SHOW_W_RULE_ATTR,
49
+ SHOW_WHEN_ATTR,
50
+ SHOW_WHERE_ATTR,
51
+ WHEN_OR_ELAPSED_ATTR,
52
+ WHERE_ATTR,
53
+ )
54
+ from logger_36.extension.file import NewTemporaryFile
55
+ from logger_36.extension.sentinel import NOT_PASSED
46
56
  from logger_36.task.format.message import MessageWithActualExpected
47
- from logger_36.task.measure.chronos import ElapsedTime
57
+ from logger_36.task.measure.chronos import FormattedElapsedTime
48
58
  from logger_36.task.measure.memory import CurrentUsage as CurrentMemoryUsage
49
59
  from logger_36.type.handler import extension_t as handler_extension_t
50
60
  from logger_36.type.handler import handler_h as base_handler_h
@@ -63,6 +73,8 @@ logger_handle_raw_h = h.Callable[[l.LogRecord], None]
63
73
  logger_handle_with_self_h = h.Callable[[l.Logger, l.LogRecord], None]
64
74
  logger_handle_h = logger_handle_raw_h | logger_handle_with_self_h
65
75
 
76
+ MAIN_PROCESS_NAME = "MainProcess"
77
+
66
78
 
67
79
  @d.dataclass(slots=True, repr=False, eq=False)
68
80
  class logger_t(base_t):
@@ -87,7 +99,9 @@ class logger_t(base_t):
87
99
  last_message_date: date_t = d.field(init=False, default=DATE_ORIGIN)
88
100
  memory_usages: list[tuple[str, int]] = d.field(init=False, default_factory=list)
89
101
  context_levels: list[str] = d.field(init=False, default_factory=list)
90
- staged_issues: list[issue_t] = d.field(init=False, default_factory=list)
102
+ staged_issues: list[tuple[issue_t, bool]] = d.field(
103
+ init=False, default_factory=list
104
+ )
91
105
  intercepted_wrn_handle: logger_handle_h | None = d.field(init=False, default=None)
92
106
  intercepted_log_handles: dict[str, logger_handle_h] = d.field(
93
107
  init=False, default_factory=dict
@@ -97,6 +111,8 @@ class logger_t(base_t):
97
111
  # Used only until the first handler is added (see AddHandler).
98
112
  _should_activate_log_interceptions: bool = d.field(init=False, default=False)
99
113
 
114
+ log_server: log_server_t | None = d.field(init=False, default=None)
115
+
100
116
  name_: d.InitVar[str | None] = None
101
117
  level_: d.InitVar[int] = l.NOTSET
102
118
  activate_wrn_interceptions: d.InitVar[bool] = True
@@ -109,6 +125,21 @@ class logger_t(base_t):
109
125
  FormattedEntry = lambda _: f"{_[0]}: {_[1].replace('\n', '↲ ')}"
110
126
  return "\n".join(map(FormattedEntry, self.history.items()))
111
127
 
128
+ @property
129
+ def records(self) -> records_h | None:
130
+ """"""
131
+ return logger_t.Records(self)
132
+
133
+ @staticmethod
134
+ def Records(logger: base_t | l.Logger, /) -> records_h | None:
135
+ """"""
136
+ for handler in logger.handlers:
137
+ output = getattr(handler, "records", None)
138
+ if memory_handler_t.AreRecords(output):
139
+ return output
140
+
141
+ return None
142
+
112
143
  @property
113
144
  def intercepts_warnings(self) -> bool:
114
145
  """"""
@@ -156,6 +187,8 @@ class logger_t(base_t):
156
187
  activate_exc_interceptions: bool,
157
188
  ) -> None:
158
189
  """"""
190
+ assert prll.current_process().name == MAIN_PROCESS_NAME
191
+
159
192
  if name_ is None:
160
193
  name_ = f"{type(self).__name__}:{hex(id(self))[2:]}"
161
194
 
@@ -185,38 +218,39 @@ class logger_t(base_t):
185
218
 
186
219
  def handle(self, record: l.LogRecord, /) -> None:
187
220
  """"""
188
- elapsed_time, now = ElapsedTime(should_return_now=True)
189
-
221
+ now = date_time_t.now()
190
222
  if (date := now.date()) != self.last_message_date:
191
223
  self._AcknowledgeDateChange(date)
192
224
 
225
+ level = record.levelno
226
+
193
227
  # When.
194
- if now - self.last_message_now > LONG_ENOUGH:
195
- w_or_e = now.strftime(TIME_FORMAT)
196
- else:
197
- w_or_e = f"{ELAPSED_TIME_SEPARATOR}{elapsed_time:.<{TIME_LENGTH_m_1}}"
198
- record.when_or_elapsed = w_or_e
228
+ if getattr(record, SHOW_WHEN_ATTR, True):
229
+ if now - self.last_message_now > LONG_ENOUGH:
230
+ w_or_e = f"{now:{TIME_FORMAT}}"
231
+ else:
232
+ w_or_e = FormattedElapsedTime(now) # or: f"{[...]:.<{TIME_LENGTH}}".
233
+ setattr(record, WHEN_OR_ELAPSED_ATTR, w_or_e)
199
234
  self.last_message_now = now
200
235
 
201
236
  # Where.
202
- should_show_where = getattr(record, SHOW_WHERE_ATTR, record.levelno != l.INFO)
237
+ should_show_where = getattr(record, SHOW_WHERE_ATTR, level != l.INFO)
203
238
  if should_show_where or self.should_monitor_memory_usage:
204
- where = RecordLocation(record, should_show_where)
205
- else:
206
- where = None
239
+ where = f"{record.pathname}:{record.funcName}:{record.lineno}"
240
+ if should_show_where:
241
+ setattr(record, WHERE_ATTR, where)
242
+ if self.should_monitor_memory_usage:
243
+ self.memory_usages.append((where, CurrentMemoryUsage()))
207
244
 
208
245
  # What.
209
246
  if not isinstance(record.msg, str):
210
247
  record.msg = str(record.msg)
211
248
 
212
249
  base_t.handle(self, record)
213
- self.n_events[record.levelno] += 1
250
+ self.n_events[level] += 1
214
251
 
215
- if self.should_monitor_memory_usage:
216
- self.memory_usages.append((where, CurrentMemoryUsage()))
217
-
218
- if (self.exit_on_critical and (record.levelno is l.CRITICAL)) or (
219
- self.exit_on_error and (record.levelno is l.ERROR)
252
+ if (self.exit_on_critical and (level is l.CRITICAL)) or (
253
+ self.exit_on_error and (level is l.ERROR)
220
254
  ):
221
255
  # Also works if self.exit_on_error and record.levelno is l.CRITICAL since
222
256
  # __post_init__ set self.exit_on_critical if self.exit_on_error.
@@ -230,7 +264,7 @@ class logger_t(base_t):
230
264
  {
231
265
  "name": self.name,
232
266
  "levelno": l.INFO, # For management by logging.Logger.handle.
233
- "msg": f"DATE: {date.strftime(DATE_FORMAT)}",
267
+ "msg": f"DATE: {date:{DATE_FORMAT}}",
234
268
  SHOW_W_RULE_ATTR: True,
235
269
  }
236
270
  )
@@ -390,6 +424,27 @@ class logger_t(base_t):
390
424
  """"""
391
425
  self.AddHandler(file_handler_t, path=path)
392
426
 
427
+ def MakeMultiSafe(self) -> None:
428
+ """
429
+ Should not be called until after all desired handlers have been added (as a
430
+ better-then-nothing check, it is checked that the logger has at least one
431
+ handler). If handlers are added passed this call, execution might freeze or
432
+ crash.
433
+ """
434
+ assert self.log_server is None
435
+ assert self.hasHandlers()
436
+
437
+ handlers = tuple(self.handlers) # Making a copy is necessary.
438
+ for handler in handlers:
439
+ self.removeHandler(handler)
440
+
441
+ queue = prll.Queue()
442
+
443
+ self.addHandler(queue_handler_t(queue))
444
+
445
+ self.log_server = log_server_t(queue, *handlers)
446
+ self.log_server.start()
447
+
393
448
  def __call__(self, *args, **kwargs) -> None:
394
449
  """
395
450
  For a print-like calling for print-based debugging.
@@ -401,7 +456,7 @@ class logger_t(base_t):
401
456
  path = path_t(details.filename)
402
457
  if path.is_relative_to(USER_FOLDER):
403
458
  path = path.relative_to(USER_FOLDER)
404
- where = f"{str(path.with_suffix(''))}.{details.function}.{details.lineno}"
459
+ where = f"{str(path.with_suffix(''))}:{details.function}:{details.lineno}"
405
460
 
406
461
  self.info(separator.join(map(str, args)) + f"\n{WHERE_SEPARATOR} " + where)
407
462
 
@@ -420,7 +475,7 @@ class logger_t(base_t):
420
475
  """"""
421
476
  if isinstance(level, str):
422
477
  level = l.getLevelNamesMapping()[level.upper()]
423
- message = MessageWithActualExpected(
478
+ message, has_actual_expected = MessageWithActualExpected(
424
479
  message,
425
480
  actual=actual,
426
481
  expected=expected,
@@ -428,19 +483,25 @@ class logger_t(base_t):
428
483
  expected_op=expected_op,
429
484
  with_final_dot=with_final_dot,
430
485
  )
431
- self.log(level, message)
486
+ if has_actual_expected:
487
+ extra = {HAS_ACTUAL_EXPECTED_ATTR: True}
488
+ else:
489
+ extra = {}
490
+ self.log(level, message, extra=extra)
432
491
 
433
492
  def LogAsIs(self, message: str, /, *, indented: bool = False) -> None:
434
493
  """"""
435
494
  if indented:
436
495
  message = text.indent(message, LINE_INDENT)
437
496
 
497
+ emit_message_name = handler_extension_t.EmitMessage.__name__
498
+ FallbackEmitMessage = print
438
499
  for handler in self.handlers:
439
- EmitMessage = getattr(
440
- handler, handler_extension_t.EmitMessage.__name__, None
441
- )
500
+ EmitMessage = getattr(handler, emit_message_name, FallbackEmitMessage)
442
501
  if EmitMessage is not None:
443
502
  EmitMessage(message)
503
+ if EmitMessage is print:
504
+ FallbackEmitMessage = None
444
505
 
445
506
  info_raw = LogAsIs # To follow the convention of the logging methods info, error...
446
507
 
@@ -472,20 +533,30 @@ class logger_t(base_t):
472
533
  self.LogException(exception, level=l.CRITICAL)
473
534
  s.exit(1)
474
535
 
475
- def DealWithExceptionInThread(
476
- self, exc_type, exc_value, exc_traceback, _, /
477
- ) -> None:
536
+ def DealWithExceptionInThread(self, args, /) -> None:
478
537
  """"""
479
- self.DealWithException(exc_type, exc_value, exc_traceback)
538
+ self.DealWithException(args.exc_type, args.exc_value, args.exc_traceback)
480
539
 
481
540
  def DisplayRule(
482
541
  self, /, *, message: str | None = None, color: str = "white"
483
542
  ) -> None:
484
543
  """"""
485
- for handler in self.handlers:
486
- EmitRule = getattr(handler, handler_extension_t.EmitRule.__name__, None)
487
- if EmitRule is not None:
488
- EmitRule(text=message, color=color)
544
+ if message is None:
545
+ message = ""
546
+ record = l.makeLogRecord(
547
+ {
548
+ "name": self.name,
549
+ "levelno": l.INFO, # For management by logging.Logger.handle.
550
+ "msg": message,
551
+ SHOW_W_RULE_ATTR: True,
552
+ }
553
+ )
554
+ base_t.handle(self, record)
555
+ # emit_rule_name = handler_extension_t.EmitRule.__name__
556
+ # for handler in self.handlers:
557
+ # EmitRule = getattr(handler, emit_rule_name, None)
558
+ # if EmitRule is not None:
559
+ # EmitRule(text=message, color=color)
489
560
 
490
561
  def AddContextLevel(self, new_level: str, /) -> None:
491
562
  """"""
@@ -527,7 +598,9 @@ class logger_t(base_t):
527
598
  )
528
599
  self.staged_issues.append(issue)
529
600
 
530
- def PopIssues(self, /, *, should_remove_context: bool = False) -> list[str]:
601
+ def PopIssues(
602
+ self, /, *, should_remove_context: bool = False
603
+ ) -> list[tuple[str, bool]]:
531
604
  """"""
532
605
  if not self.has_staged_issues:
533
606
  return []
@@ -539,10 +612,10 @@ class logger_t(base_t):
539
612
  else:
540
613
  separator = ISSUE_LEVEL_SEPARATOR
541
614
  separator_length = separator.__len__()
542
- for issue in self.staged_issues:
615
+ for issue, has_actual_expected in self.staged_issues:
543
616
  start_idx = issue.find(separator)
544
617
  issue = issue[(start_idx + separator_length) :]
545
- output.append(issue)
618
+ output.append((issue, has_actual_expected))
546
619
 
547
620
  self.staged_issues.clear()
548
621
 
@@ -564,13 +637,16 @@ class logger_t(base_t):
564
637
  "Invalid commit order",
565
638
  actual=order,
566
639
  expected=f"One of {str(ORDER)[1:-1]}",
567
- )
640
+ )[0]
568
641
  )
569
642
 
570
643
  if order == "when":
571
644
  issues = self.staged_issues
572
645
  else: # order == "context"
573
- issues = sorted(self.staged_issues, key=lambda _: _.context)
646
+ issues = sorted(
647
+ self.staged_issues,
648
+ key=lambda _: _[0].split(ISSUE_LEVEL_SEPARATOR, maxsplit=1)[1],
649
+ )
574
650
  """
575
651
  Format issues as an exception:
576
652
  try:
@@ -582,20 +658,43 @@ class logger_t(base_t):
582
658
  formatted = "\n".join(lines)
583
659
  """
584
660
 
585
- hide_where = {SHOW_WHERE_ATTR: False}
661
+ extra = {SHOW_WHERE_ATTR: False}
586
662
  if unified:
587
- level, _ = issues[0].split(ISSUE_LEVEL_SEPARATOR, maxsplit=1)
663
+ level, _ = issues[0][0].split(ISSUE_LEVEL_SEPARATOR, maxsplit=1)
588
664
  wo_level = []
589
- for issue in issues:
665
+ any_has_actual_expected = False
666
+ for issue, has_actual_expected in issues:
590
667
  _, issue = issue.split(ISSUE_LEVEL_SEPARATOR, maxsplit=1)
668
+ if has_actual_expected:
669
+ any_has_actual_expected = True
591
670
  wo_level.append(issue)
592
- self.log(int(level), "\n".join(wo_level), stacklevel=2, extra=hide_where)
671
+ if any_has_actual_expected:
672
+ extra[HAS_ACTUAL_EXPECTED_ATTR] = True
673
+ self.log(int(level), "\n".join(wo_level), stacklevel=2, extra=extra)
593
674
  else:
594
- for issue in issues:
675
+ for issue, has_actual_expected in issues:
595
676
  level, issue = issue.split(ISSUE_LEVEL_SEPARATOR, maxsplit=1)
596
- self.log(int(level), issue, stacklevel=2, extra=hide_where)
677
+ if has_actual_expected:
678
+ extra[HAS_ACTUAL_EXPECTED_ATTR] = True
679
+ self.log(int(level), issue, stacklevel=2, extra=extra)
680
+ if has_actual_expected:
681
+ del extra[HAS_ACTUAL_EXPECTED_ATTR]
597
682
  self.staged_issues.clear()
598
683
 
684
+ def StoragePath(self, suffix: str, /) -> path_t:
685
+ """
686
+ Use as staticmethod if needed.
687
+ """
688
+ for handler in self.handlers:
689
+ if (path := getattr(handler, "baseFilename", None)) is not None:
690
+ output = path_t(path).with_suffix(suffix)
691
+ if output.exists():
692
+ output = NewTemporaryFile(suffix)
693
+
694
+ return output
695
+
696
+ return NewTemporaryFile(suffix)
697
+
599
698
  def __enter__(self) -> None:
600
699
  """"""
601
700
  pass
@@ -611,6 +710,13 @@ class logger_t(base_t):
611
710
  _ = self.context_levels.pop()
612
711
  return False
613
712
 
713
+ def __del__(self) -> None:
714
+ """"""
715
+ if (prll.current_process().name == MAIN_PROCESS_NAME) and (
716
+ self.log_server is not None
717
+ ):
718
+ self.log_server.stop()
719
+
614
720
 
615
721
  def _HandleForWarnings(interceptor: base_t, /) -> logger_handle_h:
616
722
  """"""
logger_36/type/loggers.py CHANGED
@@ -22,7 +22,6 @@ class loggers_t(dict[h.Hashable, logger_t]):
22
22
  *,
23
23
  name: str | None = None,
24
24
  level: int = l.NOTSET,
25
- should_record_messages: bool = False,
26
25
  exit_on_error: bool = False,
27
26
  exit_on_critical: bool = False,
28
27
  activate_wrn_interceptions: bool = True,
@@ -31,7 +30,6 @@ class loggers_t(dict[h.Hashable, logger_t]):
31
30
  ) -> None:
32
31
  """"""
33
32
  logger = logger_t(
34
- should_record_messages=should_record_messages,
35
33
  exit_on_error=exit_on_error,
36
34
  exit_on_critical=exit_on_critical,
37
35
  name_=name,
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.24"
7
+ __version__ = "2025.26"
8
8
 
9
9
  """
10
10
  COPYRIGHT NOTICE