logging-tee 1.0.0__tar.gz
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.
- logging_tee-1.0.0/PKG-INFO +51 -0
- logging_tee-1.0.0/README.md +43 -0
- logging_tee-1.0.0/pyproject.toml +14 -0
- logging_tee-1.0.0/setup.cfg +4 -0
- logging_tee-1.0.0/src/logging_tee/__init__.py +3 -0
- logging_tee-1.0.0/src/logging_tee/tee.py +324 -0
- logging_tee-1.0.0/src/logging_tee.egg-info/PKG-INFO +51 -0
- logging_tee-1.0.0/src/logging_tee.egg-info/SOURCES.txt +9 -0
- logging_tee-1.0.0/src/logging_tee.egg-info/dependency_links.txt +1 -0
- logging_tee-1.0.0/src/logging_tee.egg-info/requires.txt +1 -0
- logging_tee-1.0.0/src/logging_tee.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logging-tee
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Logging utilities that work well with tqdm
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: tqdm
|
|
8
|
+
|
|
9
|
+
# Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install logging-tee
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
# Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
setup_logger(log_file="output.log", level=logging.DEBUG)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
for i in tqdm(
|
|
23
|
+
range(2),
|
|
24
|
+
desc="Processing",
|
|
25
|
+
):
|
|
26
|
+
time.sleep(0.03)
|
|
27
|
+
logger.info("Processing item %d", i)
|
|
28
|
+
|
|
29
|
+
for outer_idx in tqdm(
|
|
30
|
+
range(2),
|
|
31
|
+
desc="Outer",
|
|
32
|
+
):
|
|
33
|
+
for inner_idx in tqdm(
|
|
34
|
+
range(3),
|
|
35
|
+
desc=f"Inner {outer_idx}",
|
|
36
|
+
):
|
|
37
|
+
time.sleep(0.02)
|
|
38
|
+
if inner_idx % 5 == 0:
|
|
39
|
+
print(f"Nested step outer={outer_idx} inner={inner_idx}")
|
|
40
|
+
logger.info(f"Completed outer={outer_idx}")
|
|
41
|
+
|
|
42
|
+
print("from print statement")
|
|
43
|
+
print("multiple lines\nfrom print statement 2")
|
|
44
|
+
logger.debug("Single line")
|
|
45
|
+
logger.debug("Multiple lines:\nnext line")
|
|
46
|
+
logger.debug("Another single line")
|
|
47
|
+
logger.debug("Multiple lines:\n%s", "next line\nnext line 2")
|
|
48
|
+
logger.warning("Warning message\nwith multiple lines\nand should be logged properly.")
|
|
49
|
+
logger.error("Error message\nwith multiple lines\nand should be logged properly.")
|
|
50
|
+
raise ValueError("This is an error message\nwith multiple lines\nand should be logged properly.")
|
|
51
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
pip install logging-tee
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
# Usage
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
setup_logger(log_file="output.log", level=logging.DEBUG)
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
for i in tqdm(
|
|
15
|
+
range(2),
|
|
16
|
+
desc="Processing",
|
|
17
|
+
):
|
|
18
|
+
time.sleep(0.03)
|
|
19
|
+
logger.info("Processing item %d", i)
|
|
20
|
+
|
|
21
|
+
for outer_idx in tqdm(
|
|
22
|
+
range(2),
|
|
23
|
+
desc="Outer",
|
|
24
|
+
):
|
|
25
|
+
for inner_idx in tqdm(
|
|
26
|
+
range(3),
|
|
27
|
+
desc=f"Inner {outer_idx}",
|
|
28
|
+
):
|
|
29
|
+
time.sleep(0.02)
|
|
30
|
+
if inner_idx % 5 == 0:
|
|
31
|
+
print(f"Nested step outer={outer_idx} inner={inner_idx}")
|
|
32
|
+
logger.info(f"Completed outer={outer_idx}")
|
|
33
|
+
|
|
34
|
+
print("from print statement")
|
|
35
|
+
print("multiple lines\nfrom print statement 2")
|
|
36
|
+
logger.debug("Single line")
|
|
37
|
+
logger.debug("Multiple lines:\nnext line")
|
|
38
|
+
logger.debug("Another single line")
|
|
39
|
+
logger.debug("Multiple lines:\n%s", "next line\nnext line 2")
|
|
40
|
+
logger.warning("Warning message\nwith multiple lines\nand should be logged properly.")
|
|
41
|
+
logger.error("Error message\nwith multiple lines\nand should be logged properly.")
|
|
42
|
+
raise ValueError("This is an error message\nwith multiple lines\nand should be logged properly.")
|
|
43
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "logging-tee"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Logging utilities that work well with tqdm"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = ["tqdm"]
|
|
12
|
+
|
|
13
|
+
[tool.setuptools.packages.find]
|
|
14
|
+
where = ["src"]
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import copy
|
|
8
|
+
import tqdm.std as tqdm_std
|
|
9
|
+
import tqdm.auto as tqdm_auto
|
|
10
|
+
from tqdm.auto import tqdm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MultilineMixin:
|
|
14
|
+
def iter_formatted_lines(self, record):
|
|
15
|
+
# Picks formatter: self.formatter or a default plain formatter
|
|
16
|
+
fmt = self.formatter or logging.Formatter("%(message)s")
|
|
17
|
+
# Copies the record to avoid mutating shared state used by other handlers
|
|
18
|
+
base_record = copy.copy(record)
|
|
19
|
+
# If you do not clear exec_info/exc_text before formatting line-by-line, traceback can be appended repeatedly or formatting can look duplicated/inconsistent across handlers.
|
|
20
|
+
base_record.args = None
|
|
21
|
+
base_record.exc_info = None
|
|
22
|
+
base_record.exc_text = None
|
|
23
|
+
base_record.stack_info = None
|
|
24
|
+
|
|
25
|
+
# Splits normal message into lines, formats each line as its own full log record.
|
|
26
|
+
message_lines = record.getMessage().splitlines() or [record.getMessage()]
|
|
27
|
+
for line in message_lines:
|
|
28
|
+
line_record = copy.copy(base_record)
|
|
29
|
+
line_record.msg = line
|
|
30
|
+
yield fmt.format(line_record)
|
|
31
|
+
|
|
32
|
+
# If exception exists, formats traceback text and emits each traceback line as its own fully formatted log line.
|
|
33
|
+
if record.exc_info:
|
|
34
|
+
exc_text = fmt.formatException(record.exc_info)
|
|
35
|
+
for line in exc_text.splitlines():
|
|
36
|
+
line_record = copy.copy(base_record)
|
|
37
|
+
line_record.msg = line
|
|
38
|
+
yield fmt.format(line_record)
|
|
39
|
+
|
|
40
|
+
# Same for stack_info lines.
|
|
41
|
+
if record.stack_info:
|
|
42
|
+
stack_text = fmt.formatStack(record.stack_info)
|
|
43
|
+
for line in stack_text.splitlines():
|
|
44
|
+
line_record = copy.copy(base_record)
|
|
45
|
+
line_record.msg = line
|
|
46
|
+
yield fmt.format(line_record)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TqdmLoggingHandler(MultilineMixin, logging.Handler):
|
|
50
|
+
def __init__(self, level=logging.NOTSET, stream=None):
|
|
51
|
+
"""
|
|
52
|
+
Why `sys.stderr`: Because tqdm and logging are sharing terminal space, `stderr` is the safer channel.
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(level)
|
|
55
|
+
self.stream = sys.__stderr__ if stream is None else stream
|
|
56
|
+
|
|
57
|
+
def emit(self, record):
|
|
58
|
+
"""
|
|
59
|
+
Normal `StreamHandler` writes directly to terminal, which can overwrite or break tqdm lines.
|
|
60
|
+
`TqdmLoggingHandler` uses `tqdm.write(...)`, which is tqdm-aware and prints messages around the bar safely.
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
for line in self.iter_formatted_lines(record):
|
|
64
|
+
tqdm.write(line, file=self.stream)
|
|
65
|
+
except Exception:
|
|
66
|
+
self.handleError(record)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FileHandler(MultilineMixin, logging.FileHandler):
|
|
70
|
+
def emit(self, record):
|
|
71
|
+
try:
|
|
72
|
+
lines = self.iter_formatted_lines(record)
|
|
73
|
+
|
|
74
|
+
self.acquire()
|
|
75
|
+
try:
|
|
76
|
+
if self.stream is None:
|
|
77
|
+
self.stream = self._open()
|
|
78
|
+
for line in lines:
|
|
79
|
+
self.stream.write(line + self.terminator)
|
|
80
|
+
self.flush()
|
|
81
|
+
finally:
|
|
82
|
+
self.release()
|
|
83
|
+
except Exception:
|
|
84
|
+
self.handleError(record)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LineBufferLoggerWriter:
|
|
88
|
+
def __init__(self, logger, level):
|
|
89
|
+
self.logger = logger
|
|
90
|
+
self.level = level
|
|
91
|
+
self.buffer = ""
|
|
92
|
+
|
|
93
|
+
def write(self, message):
|
|
94
|
+
"""
|
|
95
|
+
`print()` and many libraries call `stream.write(...)`.
|
|
96
|
+
It buffers text until it sees `\n`, then logs complete lines with `self.logger.log(self.level, line)`.
|
|
97
|
+
"""
|
|
98
|
+
if not message:
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
self.buffer += message
|
|
102
|
+
while "\n" in self.buffer:
|
|
103
|
+
# Why buffer: `print()`/writers may send partial chunks, and logging partial fragments would create broken lines.
|
|
104
|
+
line, self.buffer = self.buffer.split("\n", 1)
|
|
105
|
+
line = line.rstrip("\r")
|
|
106
|
+
if line:
|
|
107
|
+
self.logger.log(self.level, line)
|
|
108
|
+
return len(message)
|
|
109
|
+
|
|
110
|
+
def flush(self):
|
|
111
|
+
"""
|
|
112
|
+
Some code calls `flush()` explicitly (or `print(.., flush=True)`).
|
|
113
|
+
This forces any leftover buffered text (without trailing newline) to be logged.
|
|
114
|
+
Without it, the last partial line might never appear in logs.
|
|
115
|
+
"""
|
|
116
|
+
if self.buffer:
|
|
117
|
+
line = self.buffer.rstrip("\r")
|
|
118
|
+
if line:
|
|
119
|
+
self.logger.log(self.level, line)
|
|
120
|
+
self.buffer = ""
|
|
121
|
+
|
|
122
|
+
def isatty(self):
|
|
123
|
+
"""
|
|
124
|
+
Many tools check `stream.isatty()` to decide whether to use terminal UI behaviors (colors, progress animations, carriage returns).
|
|
125
|
+
Returning `False` tells them "this is not an interactive terminal".
|
|
126
|
+
That avoids TTY-specific formatting on redirected stdout.
|
|
127
|
+
"""
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _log_tqdm_snapshot(logger, pbar, level=logging.INFO, event="progress"):
|
|
132
|
+
if pbar.disable:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
n = pbar.n
|
|
136
|
+
total = pbar.total
|
|
137
|
+
elapsed = pbar.format_dict.get("elapsed", 0.0) or 0.0
|
|
138
|
+
rate = pbar.format_dict.get("rate", None)
|
|
139
|
+
|
|
140
|
+
if rate is None:
|
|
141
|
+
rate = (n / elapsed) if elapsed > 0 else 0.0
|
|
142
|
+
|
|
143
|
+
percent = (100.0 * n / total) if total else 0.0
|
|
144
|
+
remaining = ((total - n) / rate) if (total and rate > 0) else float("inf")
|
|
145
|
+
remaining_text = "unknown" if remaining == float("inf") else f"{remaining:.1f}s"
|
|
146
|
+
elapsed_text = f"{elapsed:.1f}s"
|
|
147
|
+
|
|
148
|
+
desc = (getattr(pbar, "desc", "") or "tqdm").strip() or "tqdm"
|
|
149
|
+
logger.log(
|
|
150
|
+
level,
|
|
151
|
+
"tqdm[%s] %s: %d/%d (%.1f%%), elapsed %s, %.2f it/s, ETA %s",
|
|
152
|
+
desc,
|
|
153
|
+
event,
|
|
154
|
+
n,
|
|
155
|
+
total if total is not None else -1,
|
|
156
|
+
percent,
|
|
157
|
+
elapsed_text,
|
|
158
|
+
rate,
|
|
159
|
+
remaining_text,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def install_tqdm_logging(logger, interval_seconds=0.5, level=logging.INFO):
|
|
164
|
+
cls = tqdm_std.tqdm
|
|
165
|
+
|
|
166
|
+
if not hasattr(cls, "_auto_log_original_init"):
|
|
167
|
+
cls._auto_log_original_init = cls.__init__
|
|
168
|
+
cls._auto_log_original_update = cls.update
|
|
169
|
+
cls._auto_log_original_close = cls.close
|
|
170
|
+
|
|
171
|
+
def _patched_init(self, *args, **kwargs):
|
|
172
|
+
auto_assign_position = getattr(cls, "_auto_assign_position", False)
|
|
173
|
+
if auto_assign_position and kwargs.get("position", None) is None:
|
|
174
|
+
active_positions = set()
|
|
175
|
+
for inst in list(getattr(cls, "_instances", [])):
|
|
176
|
+
pos = getattr(inst, "pos", None)
|
|
177
|
+
if pos is None:
|
|
178
|
+
continue
|
|
179
|
+
try:
|
|
180
|
+
active_positions.add(abs(int(pos)))
|
|
181
|
+
except (TypeError, ValueError):
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
if active_positions:
|
|
185
|
+
kwargs["position"] = max(active_positions) + 1
|
|
186
|
+
|
|
187
|
+
cls._auto_log_original_init(self, *args, **kwargs)
|
|
188
|
+
self._auto_log_last_snapshot = time.monotonic()
|
|
189
|
+
|
|
190
|
+
def _patched_update(self, n=1):
|
|
191
|
+
result = cls._auto_log_original_update(self, n)
|
|
192
|
+
|
|
193
|
+
if self.disable:
|
|
194
|
+
return result
|
|
195
|
+
|
|
196
|
+
logger_obj = getattr(cls, "_auto_log_logger", None)
|
|
197
|
+
interval = getattr(cls, "_auto_log_interval", 0.5)
|
|
198
|
+
log_level = getattr(cls, "_auto_log_level", logging.INFO)
|
|
199
|
+
|
|
200
|
+
if logger_obj is None:
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
now = time.monotonic()
|
|
204
|
+
last = getattr(self, "_auto_log_last_snapshot", now)
|
|
205
|
+
is_final_step = self.total is not None and self.n >= self.total
|
|
206
|
+
if is_final_step or (now - last) >= interval:
|
|
207
|
+
_log_tqdm_snapshot(logger=logger_obj, pbar=self, level=log_level)
|
|
208
|
+
self._auto_log_last_snapshot = now
|
|
209
|
+
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
def _patched_close(self):
|
|
213
|
+
try:
|
|
214
|
+
logger_obj = getattr(cls, "_auto_log_logger", None)
|
|
215
|
+
log_level = getattr(cls, "_auto_log_level", logging.INFO)
|
|
216
|
+
if logger_obj is not None:
|
|
217
|
+
_log_tqdm_snapshot(logger=logger_obj, pbar=self, level=log_level, event="done")
|
|
218
|
+
finally:
|
|
219
|
+
return cls._auto_log_original_close(self)
|
|
220
|
+
|
|
221
|
+
cls.__init__ = _patched_init
|
|
222
|
+
cls.update = _patched_update
|
|
223
|
+
cls.close = _patched_close
|
|
224
|
+
|
|
225
|
+
cls._auto_log_logger = logger
|
|
226
|
+
cls._auto_log_interval = interval_seconds
|
|
227
|
+
cls._auto_log_level = level
|
|
228
|
+
cls._auto_assign_position = True
|
|
229
|
+
|
|
230
|
+
tqdm_auto.tqdm = cls
|
|
231
|
+
globals()["tqdm"] = cls
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def setup_logger(
|
|
235
|
+
log_file,
|
|
236
|
+
name=__name__,
|
|
237
|
+
level=logging.INFO,
|
|
238
|
+
capture_print=True,
|
|
239
|
+
capture_uncaught_exceptions=True,
|
|
240
|
+
auto_log_tqdm=True,
|
|
241
|
+
tqdm_log_interval_seconds=1,
|
|
242
|
+
tqdm_auto_assign_position=True,
|
|
243
|
+
):
|
|
244
|
+
logger = logging.getLogger(name)
|
|
245
|
+
logger.setLevel(level)
|
|
246
|
+
# Useful when setup code runs multiple times, otherwise handlers stack and the same
|
|
247
|
+
# log can print/write multiple times.
|
|
248
|
+
logger.handlers.clear()
|
|
249
|
+
# Stops this logger from forwarding records to its parent logger (often the root logger).
|
|
250
|
+
# If `True`, the message is handled by this logger's handlers and parent handlers,
|
|
251
|
+
# which often causes double printing.
|
|
252
|
+
logger.propagate = False
|
|
253
|
+
|
|
254
|
+
fmt = logging.Formatter("%(asctime)s %(name)s %(levelname)-9s %(message)s")
|
|
255
|
+
|
|
256
|
+
dirname = os.path.dirname(log_file)
|
|
257
|
+
if dirname:
|
|
258
|
+
os.makedirs(dirname, exist_ok=True)
|
|
259
|
+
file_handler = FileHandler(log_file, mode="w", encoding="utf-8")
|
|
260
|
+
file_handler.setLevel(level)
|
|
261
|
+
file_handler.setFormatter(fmt)
|
|
262
|
+
logger.addHandler(file_handler)
|
|
263
|
+
|
|
264
|
+
console_handler = TqdmLoggingHandler(level=level, stream=sys.__stderr__)
|
|
265
|
+
console_handler.setFormatter(fmt)
|
|
266
|
+
logger.addHandler(console_handler)
|
|
267
|
+
|
|
268
|
+
if capture_print:
|
|
269
|
+
# replaces `sys.stdout` with `LineBuferLoggerWriter`, so `print(...)` becomes logger `INFO`
|
|
270
|
+
sys.stdout = LineBufferLoggerWriter(logger=logger, level=logging.INFO)
|
|
271
|
+
|
|
272
|
+
if capture_uncaught_exceptions:
|
|
273
|
+
def _excepthook(exc_type, exc_value, exc_traceback):
|
|
274
|
+
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
|
|
275
|
+
|
|
276
|
+
sys.excepthook = _excepthook
|
|
277
|
+
|
|
278
|
+
if auto_log_tqdm:
|
|
279
|
+
install_tqdm_logging(
|
|
280
|
+
logger,
|
|
281
|
+
interval_seconds=tqdm_log_interval_seconds,
|
|
282
|
+
level=logging.INFO,
|
|
283
|
+
)
|
|
284
|
+
tqdm_std.tqdm._auto_assign_position = tqdm_auto_assign_position
|
|
285
|
+
|
|
286
|
+
return logger
|
|
287
|
+
|
|
288
|
+
# %%
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
setup_logger(log_file="output.log", level=logging.DEBUG)
|
|
292
|
+
logger = logging.getLogger(__name__)
|
|
293
|
+
|
|
294
|
+
for i in tqdm(
|
|
295
|
+
range(2),
|
|
296
|
+
desc="Processing",
|
|
297
|
+
):
|
|
298
|
+
time.sleep(0.03)
|
|
299
|
+
logger.info("Processing item %d", i)
|
|
300
|
+
|
|
301
|
+
for outer_idx in tqdm(
|
|
302
|
+
range(2),
|
|
303
|
+
desc="Outer",
|
|
304
|
+
):
|
|
305
|
+
for inner_idx in tqdm(
|
|
306
|
+
range(3),
|
|
307
|
+
desc=f"Inner {outer_idx}",
|
|
308
|
+
):
|
|
309
|
+
time.sleep(0.02)
|
|
310
|
+
if inner_idx % 5 == 0:
|
|
311
|
+
print(f"Nested step outer={outer_idx} inner={inner_idx}")
|
|
312
|
+
logger.info(f"Completed outer={outer_idx}")
|
|
313
|
+
|
|
314
|
+
print("from print statement")
|
|
315
|
+
print("multiple lines\nfrom print statement 2")
|
|
316
|
+
logger.debug("Single line")
|
|
317
|
+
logger.debug("Multiple lines:\nnext line")
|
|
318
|
+
logger.debug("Another single line")
|
|
319
|
+
logger.debug("Multiple lines:\n%s", "next line\nnext line 2")
|
|
320
|
+
logger.warning("Warning message\nwith multiple lines\nand should be logged properly.")
|
|
321
|
+
logger.error("Error message\nwith multiple lines\nand should be logged properly.")
|
|
322
|
+
raise ValueError("This is an error message\nwith multiple lines\nand should be logged properly.")
|
|
323
|
+
|
|
324
|
+
# %%
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logging-tee
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Logging utilities that work well with tqdm
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: tqdm
|
|
8
|
+
|
|
9
|
+
# Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install logging-tee
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
# Usage
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
setup_logger(log_file="output.log", level=logging.DEBUG)
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
for i in tqdm(
|
|
23
|
+
range(2),
|
|
24
|
+
desc="Processing",
|
|
25
|
+
):
|
|
26
|
+
time.sleep(0.03)
|
|
27
|
+
logger.info("Processing item %d", i)
|
|
28
|
+
|
|
29
|
+
for outer_idx in tqdm(
|
|
30
|
+
range(2),
|
|
31
|
+
desc="Outer",
|
|
32
|
+
):
|
|
33
|
+
for inner_idx in tqdm(
|
|
34
|
+
range(3),
|
|
35
|
+
desc=f"Inner {outer_idx}",
|
|
36
|
+
):
|
|
37
|
+
time.sleep(0.02)
|
|
38
|
+
if inner_idx % 5 == 0:
|
|
39
|
+
print(f"Nested step outer={outer_idx} inner={inner_idx}")
|
|
40
|
+
logger.info(f"Completed outer={outer_idx}")
|
|
41
|
+
|
|
42
|
+
print("from print statement")
|
|
43
|
+
print("multiple lines\nfrom print statement 2")
|
|
44
|
+
logger.debug("Single line")
|
|
45
|
+
logger.debug("Multiple lines:\nnext line")
|
|
46
|
+
logger.debug("Another single line")
|
|
47
|
+
logger.debug("Multiple lines:\n%s", "next line\nnext line 2")
|
|
48
|
+
logger.warning("Warning message\nwith multiple lines\nand should be logged properly.")
|
|
49
|
+
logger.error("Error message\nwith multiple lines\nand should be logged properly.")
|
|
50
|
+
raise ValueError("This is an error message\nwith multiple lines\nand should be logged properly.")
|
|
51
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/logging_tee/__init__.py
|
|
4
|
+
src/logging_tee/tee.py
|
|
5
|
+
src/logging_tee.egg-info/PKG-INFO
|
|
6
|
+
src/logging_tee.egg-info/SOURCES.txt
|
|
7
|
+
src/logging_tee.egg-info/dependency_links.txt
|
|
8
|
+
src/logging_tee.egg-info/requires.txt
|
|
9
|
+
src/logging_tee.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tqdm
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
logging_tee
|