ft-logger 2026.1__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.
Files changed (62) hide show
  1. ft_logger/__init__.py +24 -0
  2. ft_logger/api/chronos.py +8 -0
  3. ft_logger/api/handler.py +7 -0
  4. ft_logger/api/logger.py +7 -0
  5. ft_logger/api/memory.py +12 -0
  6. ft_logger/api/message.py +8 -0
  7. ft_logger/catalog/config/optional.py +33 -0
  8. ft_logger/catalog/config/theme_on_dark.py +23 -0
  9. ft_logger/catalog/config/theme_on_light.py +23 -0
  10. ft_logger/catalog/handler/color_rich.py +159 -0
  11. ft_logger/catalog/handler/file.py +60 -0
  12. ft_logger/catalog/handler/generic.py +125 -0
  13. ft_logger/catalog/handler/http_local.py +194 -0
  14. ft_logger/catalog/handler/memory.py +65 -0
  15. ft_logger/catalog/handler/monochrome.py +51 -0
  16. ft_logger/catalog/logger/chronos.py +69 -0
  17. ft_logger/catalog/logger/gpu.py +45 -0
  18. ft_logger/catalog/logger/memory.py +120 -0
  19. ft_logger/catalog/logger/system.py +39 -0
  20. ft_logger/config/memory.py +8 -0
  21. ft_logger/config/message.py +25 -0
  22. ft_logger/config/rule.py +8 -0
  23. ft_logger/config/system.py +24 -0
  24. ft_logger/constant/chronos.py +18 -0
  25. ft_logger/constant/color.py +289 -0
  26. ft_logger/constant/error.py +26 -0
  27. ft_logger/constant/html.py +51 -0
  28. ft_logger/constant/logger.py +14 -0
  29. ft_logger/constant/memory.py +7 -0
  30. ft_logger/constant/message.py +28 -0
  31. ft_logger/constant/record.py +13 -0
  32. ft_logger/constant/rule.py +10 -0
  33. ft_logger/constant/system.py +16 -0
  34. ft_logger/constant/theme.py +7 -0
  35. ft_logger/constant/thread.py +9 -0
  36. ft_logger/extension/object.py +10 -0
  37. ft_logger/hint/handle.py +12 -0
  38. ft_logger/hint/memory.py +9 -0
  39. ft_logger/hint/message.py +9 -0
  40. ft_logger/hint/record.py +42 -0
  41. ft_logger/hint/storage.py +9 -0
  42. ft_logger/hint/theme.py +18 -0
  43. ft_logger/hint/value.py +9 -0
  44. ft_logger/instance/logger.py +13 -0
  45. ft_logger/task/conversion/color.py +70 -0
  46. ft_logger/task/conversion/record.py +110 -0
  47. ft_logger/task/format/chronos.py +36 -0
  48. ft_logger/task/format/memory.py +78 -0
  49. ft_logger/task/format/message.py +75 -0
  50. ft_logger/task/interception.py +134 -0
  51. ft_logger/task/measure/chronos.py +20 -0
  52. ft_logger/task/measure/memory.py +37 -0
  53. ft_logger/type/logger.py +1497 -0
  54. ft_logger/type/record.py +43 -0
  55. ft_logger/type/rule.py +47 -0
  56. ft_logger/type/server.py +67 -0
  57. ft_logger/type/theme.py +192 -0
  58. ft_logger/version.py +1 -0
  59. ft_logger-2026.1.dist-info/METADATA +124 -0
  60. ft_logger-2026.1.dist-info/RECORD +62 -0
  61. ft_logger-2026.1.dist-info/WHEEL +5 -0
  62. ft_logger-2026.1.dist-info/top_level.txt +1 -0
ft_logger/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ try:
8
+ from beartype.claw import beartype_this_package # noqa
9
+ except ModuleNotFoundError:
10
+ pass
11
+ else:
12
+ import site
13
+ from pathlib import Path as path_t
14
+
15
+ folder = path_t(__file__).parent
16
+ paths = site.getsitepackages() + [site.getusersitepackages()]
17
+ for path in paths:
18
+ if folder.is_relative_to(path):
19
+ break
20
+ else:
21
+ beartype_this_package()
22
+
23
+ from .version import __version__ # noqa
24
+ from .instance.logger import L # noqa
@@ -0,0 +1,8 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ from ..task.format.chronos import FormattedElapsedTime # noqa
8
+ from ..task.measure.chronos import TimeStamp # noqa
@@ -0,0 +1,7 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ from ..type.theme import theme_t # noqa
@@ -0,0 +1,7 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ from ..type.logger import logger_t # noqa
@@ -0,0 +1,12 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ from ..catalog.config.optional import MEMORY_MEASURE_IS_AVAILABLE # noqa
8
+ from ..task.format.memory import FormattedUsage as FormattedMemoryUsage # noqa
9
+ from ..task.format.memory import ( # noqa
10
+ FormattedUsageWithAutoUnit as FormattedMemoryUsageWithAutoUnit,
11
+ )
12
+ from ..task.measure.memory import CurrentUsage as CurrentMemoryUsage # noqa
@@ -0,0 +1,8 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ from ..constant.message import LINE_INDENT # noqa
8
+ from ..task.format.message import MessageWithActualExpected # noqa
@@ -0,0 +1,33 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ try:
8
+ import psutil # noqa
9
+ except ModuleNotFoundError:
10
+ MEMORY_MEASURE_IS_AVAILABLE = False
11
+ from ...constant.error import MISSING_MEMORY_MEASURE_MESSAGE
12
+ else:
13
+ MEMORY_MEASURE_IS_AVAILABLE = True
14
+ MISSING_MEMORY_MEASURE_MESSAGE = None
15
+
16
+ try:
17
+ import rich # noqa
18
+ except ModuleNotFoundError:
19
+ RICH_IS_AVAILABLE = False
20
+ from ...constant.error import MISSING_RICH_MESSAGE
21
+ else:
22
+ RICH_IS_AVAILABLE = True
23
+ MISSING_RICH_MESSAGE = None
24
+
25
+ try:
26
+ import tensorflow # noqa
27
+ import tensorrt # noqa
28
+ except ModuleNotFoundError:
29
+ GPU_LOGGING_IS_AVAILABLE = False
30
+ from ...constant.error import MISSING_GPU_LOGGING_MESSAGE
31
+ else:
32
+ GPU_LOGGING_IS_AVAILABLE = True
33
+ MISSING_GPU_LOGGING_MESSAGE = None
@@ -0,0 +1,23 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ import logging as l
8
+
9
+ TEXT_COLOR = "grey85"
10
+ WHERE_COLOR = "grey58"
11
+
12
+ LEVEL_COLOR = {
13
+ l.DEBUG: "orchid",
14
+ l.INFO: WHERE_COLOR,
15
+ l.WARNING: "yellow1",
16
+ l.ERROR: "dark_orange",
17
+ l.CRITICAL: "bright_red",
18
+ }
19
+ ACTUAL_COLOR = LEVEL_COLOR[l.CRITICAL]
20
+ EXPECTED_COLOR = "green3"
21
+ RULE_COLOR = "sky_blue3"
22
+
23
+ ALTERNATIVE_BACKGROUND = (15, 15, 15)
@@ -0,0 +1,23 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ import logging as l
8
+
9
+ TEXT_COLOR = "grey27"
10
+ WHERE_COLOR = "grey46"
11
+
12
+ LEVEL_COLOR = {
13
+ l.DEBUG: "orchid",
14
+ l.INFO: WHERE_COLOR,
15
+ l.WARNING: "light_goldenrod3",
16
+ l.ERROR: "dark_orange",
17
+ l.CRITICAL: "bright_red",
18
+ }
19
+ ACTUAL_COLOR = LEVEL_COLOR[l.CRITICAL]
20
+ EXPECTED_COLOR = "green3"
21
+ RULE_COLOR = "dodger_blue1"
22
+
23
+ ALTERNATIVE_BACKGROUND = (230, 230, 230)
@@ -0,0 +1,159 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ import logging as l
8
+ import sys as s
9
+ import typing as h
10
+
11
+ from ...config.message import ACTUAL_PATTERNS, EXPECTED_PATTERN, FALLBACK_PAGE_WIDTH
12
+ from ...constant.color import COLORS_TERMINAL_256
13
+ from ...constant.message import CONTEXT_LENGTH_p_1
14
+ from ...extension.object import DefaultNameFor
15
+ from ...task.conversion.record import MessageFromRecord
16
+ from ...type.theme import theme_t
17
+ from ..config.optional import MISSING_RICH_MESSAGE, RICH_IS_AVAILABLE
18
+
19
+ if RICH_IS_AVAILABLE:
20
+ from rich.color import Color as color_t # noqa
21
+ from rich.console import Console as console_t # noqa
22
+ from rich.markup import escape as EscapedVersion # noqa
23
+ from rich.rule import Rule as rule_t # noqa
24
+ from rich.style import Style as style_t # noqa
25
+ from rich.text import Text as text_t # noqa
26
+ from rich.traceback import install as InstallTracebackHandler # noqa
27
+
28
+ _COMMON_TRACEBACK_ARGUMENTS = ("theme", "width")
29
+ _EXCLUSIVE_TRACEBACK_ARGUMENTS = (
30
+ "extra_lines",
31
+ "indent_guides",
32
+ "locals_hide_dunder",
33
+ "locals_hide_sunder",
34
+ "locals_max_length",
35
+ "locals_max_string",
36
+ "max_framesshow_locals",
37
+ "suppress",
38
+ "word_wrap",
39
+ )
40
+
41
+ base_t = l.Handler
42
+
43
+ class color_rich_handler_t(base_t):
44
+ def __init__(self, *_, **kwargs) -> None:
45
+ """"""
46
+ base_t.__init__(self)
47
+
48
+ self.name: str = kwargs.pop("name", DefaultNameFor(self))
49
+ self.page_width: int = kwargs.pop("page_width", -1)
50
+ self.wrapping_is_disabled = True
51
+ self.theme: str | theme_t | None = kwargs.pop("theme", None)
52
+ self.console: console_t | None = None
53
+ self.should_install_traceback: bool = kwargs.pop(
54
+ "should_install_traceback", False
55
+ )
56
+ self.DoEmit: h.Callable | None = None
57
+ self.FlushStream: h.Callable | None = None
58
+
59
+ self.__post_init__(kwargs.pop("level", l.INFO), **kwargs)
60
+
61
+ def __post_init__(self, level: int, /, **kwargs) -> None:
62
+ """"""
63
+ self.setLevel(level)
64
+
65
+ if 0 < self.page_width < FALLBACK_PAGE_WIDTH:
66
+ # Prevent message width from being "unrealistically" small.
67
+ self.page_width = FALLBACK_PAGE_WIDTH
68
+ self.wrapping_is_disabled = self.page_width <= 0
69
+
70
+ if self.theme is None:
71
+ self.theme = theme_t.NewDefault("rich", "dark")
72
+ elif isinstance(self.theme, str):
73
+ self.theme = theme_t.NewDefaultFromStr(self.theme)
74
+ MakeThemeRich(self.theme)
75
+
76
+ traceback_kwargs = {}
77
+ if self.should_install_traceback:
78
+ for key in kwargs:
79
+ if key in _COMMON_TRACEBACK_ARGUMENTS:
80
+ traceback_kwargs[key] = kwargs[key]
81
+ elif key in _EXCLUSIVE_TRACEBACK_ARGUMENTS:
82
+ traceback_kwargs[key] = kwargs.pop(key)
83
+
84
+ self.console = console_t(
85
+ highlight=False, force_terminal=True, file=s.__stdout__, **kwargs
86
+ )
87
+ self.DoEmit = self.console.print
88
+ self.FlushStream = self.console.file.flush
89
+
90
+ if self.should_install_traceback:
91
+ traceback_kwargs["console"] = self.console
92
+ InstallTracebackHandler(**traceback_kwargs)
93
+
94
+ def emit(self, record: l.LogRecord, /) -> None:
95
+ """"""
96
+ message = MessageFromRecord(
97
+ record,
98
+ self.page_width,
99
+ self.wrapping_is_disabled,
100
+ self.theme,
101
+ self.Rule,
102
+ )
103
+ self.DoEmit(message, crop=False, overflow="ignore")
104
+
105
+ def flush(self) -> None:
106
+ """"""
107
+ self.FlushStream()
108
+
109
+ def Rule(
110
+ self, /, *, text: str | None = None, color: str | None = None
111
+ ) -> str | rule_t:
112
+ """"""
113
+ if color is None:
114
+ color = self.theme.rule
115
+ if text in (None, "None"):
116
+ return rule_t(style=color)
117
+ return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
118
+
119
+ def MakeThemeRich(theme: theme_t, /) -> None:
120
+ """"""
121
+ theme.SetColorizedMessageFunction(_ColorizedMessage)
122
+ if theme.should_alternate_background:
123
+ # Done here to avoid adding the Rich dependency to the theme_t implementation.
124
+ color = color_t.from_rgb(*COLORS_TERMINAL_256[theme.background_alt][2])
125
+ theme.background_alt = style_t(bgcolor=color)
126
+
127
+ def _ColorizedMessage(
128
+ theme: theme_t,
129
+ log_level: int,
130
+ when_or_elapsed_and_level: str,
131
+ message: str,
132
+ where_and_whatnot: str,
133
+ has_actual_expected: bool,
134
+ /,
135
+ ) -> text_t:
136
+ """"""
137
+ output = text_t(
138
+ f"{when_or_elapsed_and_level}{message}{where_and_whatnot}", theme.text
139
+ )
140
+
141
+ output.stylize(theme.level[log_level], end=CONTEXT_LENGTH_p_1)
142
+ if has_actual_expected:
143
+ _ = output.highlight_words(ACTUAL_PATTERNS, style=theme.actual)
144
+ _ = output.highlight_regex(EXPECTED_PATTERN, style=theme.expected)
145
+ if where_and_whatnot.__len__() > 0:
146
+ output.stylize(theme.where, start=CONTEXT_LENGTH_p_1 + message.__len__())
147
+
148
+ if theme.should_alternate_background:
149
+ if theme.should_set_background:
150
+ output.stylize(theme.background_alt)
151
+ theme.should_set_background = False
152
+ else:
153
+ theme.should_set_background = True
154
+
155
+ return output
156
+ else:
157
+ from .monochrome import monochrome_handler_t as color_rich_handler_t # noqa
158
+
159
+ s.stderr.write(MISSING_RICH_MESSAGE + "\n")
@@ -0,0 +1,60 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ import logging as l
8
+ from pathlib import Path as path_t
9
+
10
+ from ...config.message import FALLBACK_PAGE_WIDTH
11
+ from ...extension.object import DefaultNameFor
12
+ from ...task.conversion.record import MessageFromRecord
13
+ from ...type.rule import NewRule
14
+
15
+ base_t = l.FileHandler
16
+
17
+
18
+ class file_handler_t(base_t):
19
+ def __init__(self, *_, **kwargs) -> None:
20
+ """"""
21
+ path: str | path_t | None = kwargs.pop("path", None)
22
+ assert path is not None
23
+
24
+ path = path_t(path)
25
+ assert not path.exists()
26
+
27
+ parent = path.parent
28
+ if parent.exists():
29
+ assert parent.is_dir()
30
+ else:
31
+ parent.mkdir(mode=0o700, parents=True)
32
+
33
+ path.touch(mode=0o600, exist_ok=False) # Only to check writability.
34
+
35
+ base_t.__init__(self, path)
36
+
37
+ self.name: str = kwargs.pop("name", DefaultNameFor(self))
38
+ self.page_width: int = kwargs.pop("page_width", -1)
39
+ self.wrapping_is_disabled = True
40
+ self.DoEmit = self.stream.write
41
+
42
+ self.__post_init__(kwargs.pop("level", l.INFO))
43
+
44
+ def __post_init__(self, level: int, /) -> None:
45
+ """"""
46
+ self.setLevel(level)
47
+
48
+ if 0 < self.page_width < FALLBACK_PAGE_WIDTH:
49
+ # Prevent message width from being "unrealistically" small.
50
+ self.page_width = FALLBACK_PAGE_WIDTH
51
+ self.wrapping_is_disabled = self.page_width <= 0
52
+
53
+ def emit(self, record: l.LogRecord, /) -> None:
54
+ """"""
55
+ self.DoEmit(
56
+ MessageFromRecord(
57
+ record, self.page_width, self.wrapping_is_disabled, None, NewRule
58
+ )
59
+ + "\n"
60
+ )
@@ -0,0 +1,125 @@
1
+ """
2
+ SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
3
+ README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
4
+ at project source root.
5
+ """
6
+
7
+ import logging as l
8
+ import sys as s
9
+ import typing as h
10
+
11
+ from ...catalog.config.optional import RICH_IS_AVAILABLE
12
+
13
+ if RICH_IS_AVAILABLE:
14
+ from rich.console import Console as console_t
15
+ from rich.rule import Rule as rule_t
16
+ from rich.terminal_theme import DEFAULT_TERMINAL_THEME
17
+ from rich.text import Text as text_t
18
+
19
+ from .color_rich import MakeThemeRich
20
+ else:
21
+ console_t = rule_t = DEFAULT_TERMINAL_THEME = text_t = None
22
+
23
+ from ...config.message import FALLBACK_PAGE_WIDTH
24
+ from ...extension.object import DefaultNameFor
25
+ from ...task.conversion.record import MessageFromRecord
26
+ from ...type.rule import NewRule
27
+ from ...type.theme import theme_t
28
+
29
+ base_t = l.Handler
30
+
31
+
32
+ class generic_handler_t(base_t):
33
+ """
34
+ EmitMessage (h.Callable): Some callables may induce infinite loops if the logger
35
+ intercepts "print" calls, e.g. print, s.stdout.write, s.stderr.write.
36
+ """
37
+ def __init__(self, *_, **kwargs) -> None:
38
+ """"""
39
+ base_t.__init__(self)
40
+
41
+ self.name: str = kwargs.pop("name", DefaultNameFor(self))
42
+ self.page_width: int = kwargs.pop("page_width", -1)
43
+ self.wrapping_is_disabled = True
44
+ self.supports_html = False
45
+ self.theme: str | theme_t | None = kwargs.pop("theme", None)
46
+ self.console = None # console_t | None.
47
+ self.console_options = None # rich.console.ConsoleOptions | None.
48
+ self.DoEmit: h.Callable | None = kwargs.pop("EmitMessage", None)
49
+
50
+ self.__post_init__(
51
+ kwargs.pop("level", l.INFO), kwargs.pop("supports_html", False)
52
+ )
53
+
54
+ def __post_init__(self, level: int, supports_html: bool, /) -> None:
55
+ """"""
56
+ # print, s.stdout.write, s.stderr.write: Basic prevention against infinite loop.
57
+ # See class documentation.
58
+ assert self.DoEmit not in (None, print, s.stdout.write, s.stderr.write)
59
+
60
+ self.setLevel(level)
61
+
62
+ if 0 < self.page_width < FALLBACK_PAGE_WIDTH:
63
+ # Prevent message width from being "unrealistically" small.
64
+ self.page_width = FALLBACK_PAGE_WIDTH
65
+ self.wrapping_is_disabled = self.page_width <= 0
66
+
67
+ if supports_html and (console_t is not None):
68
+ self.supports_html = True
69
+ self.console = console_t(highlight=False, force_terminal=True)
70
+ self.console_options = self.console.options.update(
71
+ overflow="ignore", no_wrap=True
72
+ )
73
+
74
+ if self.theme is None:
75
+ self.theme = theme_t.NewDefault("rich", "light")
76
+ elif isinstance(self.theme, str):
77
+ self.theme = theme_t.NewDefaultFromStr(self.theme)
78
+ MakeThemeRich(self.theme)
79
+
80
+ def Rule(
81
+ self, /, *, text: str | None = None, color: str | None = None
82
+ ) -> str | rule_t:
83
+ """"""
84
+ if (color is None) and (self.theme is not None):
85
+ color = self.theme.rule
86
+
87
+ if self.supports_html:
88
+ if text in (None, "None"):
89
+ return rule_t(style=color)
90
+ return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
91
+
92
+ return NewRule(text=text, color=color)
93
+
94
+ def emit(self, record: l.LogRecord, /) -> None:
95
+ """"""
96
+ message = MessageFromRecord(
97
+ record, self.page_width, self.wrapping_is_disabled, self.theme, self.Rule
98
+ )
99
+
100
+ if self.supports_html:
101
+ segments = self.console.render(message, options=self.console_options)
102
+
103
+ # Inspired from the code of: rich.console.export_html.
104
+ html_segments = []
105
+ for text, style, _ in segments:
106
+ if text == "\n":
107
+ html_segments.append("\n")
108
+ else:
109
+ if style is not None:
110
+ style = style.get_html_style(DEFAULT_TERMINAL_THEME)
111
+ if (style is not None) and (style.__len__() > 0):
112
+ text = f'<span style="{style};">{text}</span>'
113
+ html_segments.append(text)
114
+ if html_segments[-1] == "\n":
115
+ html_segments = html_segments[:-1]
116
+
117
+ # /!\ For some reason, the widget splits the message into lines, place each
118
+ # line inside a pre tag, and set margin-bottom of the first and list lines
119
+ # to 12px. This can be seen by printing self.contents.toHtml(). To avoid the
120
+ # unwanted extra margins, margin-bottom is set to 0 below.
121
+ message = (
122
+ "<pre style='margin-bottom:0px'>" + "".join(html_segments) + "</pre>"
123
+ )
124
+
125
+ self.DoEmit(message)