logbar 0.2.0__tar.gz → 0.2.2__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.
- {logbar-0.2.0 → logbar-0.2.2}/LogBar.egg-info/PKG-INFO +9 -1
- {logbar-0.2.0 → logbar-0.2.2}/PKG-INFO +9 -1
- {logbar-0.2.0 → logbar-0.2.2}/README.md +8 -0
- {logbar-0.2.0 → logbar-0.2.2}/logbar/logbar.py +100 -6
- {logbar-0.2.0 → logbar-0.2.2}/logbar/progress.py +4 -1
- {logbar-0.2.0 → logbar-0.2.2}/pyproject.toml +1 -1
- {logbar-0.2.0 → logbar-0.2.2}/tests/test_columns.py +16 -11
- {logbar-0.2.0 → logbar-0.2.2}/tests/test_log.py +45 -0
- {logbar-0.2.0 → logbar-0.2.2}/tests/test_progress.py +25 -0
- {logbar-0.2.0 → logbar-0.2.2}/LICENSE +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/LogBar.egg-info/SOURCES.txt +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/LogBar.egg-info/dependency_links.txt +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/LogBar.egg-info/top_level.txt +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/logbar/__init__.py +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/logbar/buffer.py +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/logbar/columns.py +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/logbar/terminal.py +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/logbar/util.py +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/setup.cfg +0 -0
- {logbar-0.2.0 → logbar-0.2.2}/tests/test_example.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: LogBar
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A unified Logger and ProgressBar util with zero dependencies.
|
|
5
5
|
Author-email: ModelCloud <qubitium@modelcloud.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -84,6 +84,14 @@ log.error("cannot connect to database")
|
|
|
84
84
|
log.critical.once("fuse blown, shutting down")
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
Set a minimum output threshold per logger instance:
|
|
88
|
+
|
|
89
|
+
```py
|
|
90
|
+
log.setLevel("WARN") # accepts DEBUG/INFO/WARN/ERROR/CRIT strings
|
|
91
|
+
log.setLevel("ERROR")
|
|
92
|
+
log.setLevel(LogBar.WARNING) # alias to logging.WARNING
|
|
93
|
+
```
|
|
94
|
+
|
|
87
95
|
Typical mixed-level output (Note: Markdown cannot display ANSI colors):
|
|
88
96
|
|
|
89
97
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: LogBar
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A unified Logger and ProgressBar util with zero dependencies.
|
|
5
5
|
Author-email: ModelCloud <qubitium@modelcloud.ai>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -84,6 +84,14 @@ log.error("cannot connect to database")
|
|
|
84
84
|
log.critical.once("fuse blown, shutting down")
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
Set a minimum output threshold per logger instance:
|
|
88
|
+
|
|
89
|
+
```py
|
|
90
|
+
log.setLevel("WARN") # accepts DEBUG/INFO/WARN/ERROR/CRIT strings
|
|
91
|
+
log.setLevel("ERROR")
|
|
92
|
+
log.setLevel(LogBar.WARNING) # alias to logging.WARNING
|
|
93
|
+
```
|
|
94
|
+
|
|
87
95
|
Typical mixed-level output (Note: Markdown cannot display ANSI colors):
|
|
88
96
|
|
|
89
97
|
```
|
|
@@ -69,6 +69,14 @@ log.error("cannot connect to database")
|
|
|
69
69
|
log.critical.once("fuse blown, shutting down")
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
Set a minimum output threshold per logger instance:
|
|
73
|
+
|
|
74
|
+
```py
|
|
75
|
+
log.setLevel("WARN") # accepts DEBUG/INFO/WARN/ERROR/CRIT strings
|
|
76
|
+
log.setLevel("ERROR")
|
|
77
|
+
log.setLevel(LogBar.WARNING) # alias to logging.WARNING
|
|
78
|
+
```
|
|
79
|
+
|
|
72
80
|
Typical mixed-level output (Note: Markdown cannot display ANSI colors):
|
|
73
81
|
|
|
74
82
|
```
|
|
@@ -482,7 +482,44 @@ class LEVEL(str, Enum):
|
|
|
482
482
|
|
|
483
483
|
LEVEL_MAX_LENGTH = 5 # ERROR/DEBUG is longest at 5 chars
|
|
484
484
|
|
|
485
|
+
LEVEL_TO_LOGGING = {
|
|
486
|
+
LEVEL.DEBUG: logging.DEBUG,
|
|
487
|
+
LEVEL.INFO: logging.INFO,
|
|
488
|
+
LEVEL.WARN: logging.WARNING,
|
|
489
|
+
LEVEL.ERROR: logging.ERROR,
|
|
490
|
+
LEVEL.CRITICAL: logging.CRITICAL,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
LEVEL_NAME_TO_LOGGING = {
|
|
494
|
+
"DEBUG": logging.DEBUG,
|
|
495
|
+
"INFO": logging.INFO,
|
|
496
|
+
"WARN": logging.WARNING,
|
|
497
|
+
"WARNING": logging.WARNING,
|
|
498
|
+
"ERROR": logging.ERROR,
|
|
499
|
+
"CRIT": logging.CRITICAL,
|
|
500
|
+
"CRITICAL": logging.CRITICAL,
|
|
501
|
+
"FATAL": logging.CRITICAL,
|
|
502
|
+
"NOTSET": logging.NOTSET,
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
LOGGING_TO_LEVEL_LABEL = {
|
|
506
|
+
logging.DEBUG: LEVEL.DEBUG.value,
|
|
507
|
+
logging.INFO: LEVEL.INFO.value,
|
|
508
|
+
logging.WARNING: LEVEL.WARN.value,
|
|
509
|
+
logging.ERROR: LEVEL.ERROR.value,
|
|
510
|
+
logging.CRITICAL: LEVEL.CRITICAL.value,
|
|
511
|
+
}
|
|
512
|
+
|
|
485
513
|
class LogBar(logging.Logger):
|
|
514
|
+
NOTSET = logging.NOTSET
|
|
515
|
+
DEBUG = logging.DEBUG
|
|
516
|
+
INFO = logging.INFO
|
|
517
|
+
WARN = logging.WARNING
|
|
518
|
+
WARNING = logging.WARNING
|
|
519
|
+
ERROR = logging.ERROR
|
|
520
|
+
CRITICAL = logging.CRITICAL
|
|
521
|
+
FATAL = logging.CRITICAL
|
|
522
|
+
|
|
486
523
|
history = set()
|
|
487
524
|
history_limit = 1000
|
|
488
525
|
|
|
@@ -627,6 +664,57 @@ class LogBar(logging.Logger):
|
|
|
627
664
|
self.history = set()
|
|
628
665
|
self._history_lock = threading.Lock()
|
|
629
666
|
|
|
667
|
+
def _normalize_level(self, level: Union[LEVEL, int, str]) -> int:
|
|
668
|
+
if isinstance(level, LEVEL):
|
|
669
|
+
return LEVEL_TO_LOGGING[level]
|
|
670
|
+
|
|
671
|
+
if isinstance(level, int):
|
|
672
|
+
return level
|
|
673
|
+
|
|
674
|
+
if isinstance(level, str):
|
|
675
|
+
normalized = level.strip().upper()
|
|
676
|
+
if not normalized:
|
|
677
|
+
raise ValueError("Log level must not be empty.")
|
|
678
|
+
|
|
679
|
+
if normalized in LEVEL_NAME_TO_LOGGING:
|
|
680
|
+
return LEVEL_NAME_TO_LOGGING[normalized]
|
|
681
|
+
|
|
682
|
+
numeric = normalized
|
|
683
|
+
if normalized.startswith(("+", "-")):
|
|
684
|
+
numeric = normalized[1:]
|
|
685
|
+
|
|
686
|
+
if numeric.isdigit():
|
|
687
|
+
return int(normalized)
|
|
688
|
+
|
|
689
|
+
raise ValueError(f"Unknown log level: {level!r}")
|
|
690
|
+
|
|
691
|
+
raise TypeError(
|
|
692
|
+
"Log level must be a LEVEL enum, integer, or string name."
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
def _level_label(self, level: Union[LEVEL, int, str], normalized_level: int) -> str:
|
|
696
|
+
if isinstance(level, LEVEL):
|
|
697
|
+
return level.value
|
|
698
|
+
|
|
699
|
+
if isinstance(level, str):
|
|
700
|
+
normalized = level.strip().upper()
|
|
701
|
+
if normalized in ("WARNING", "WARN"):
|
|
702
|
+
return LEVEL.WARN.value
|
|
703
|
+
if normalized in ("CRITICAL", "FATAL", "CRIT"):
|
|
704
|
+
return LEVEL.CRITICAL.value
|
|
705
|
+
if normalized in ("DEBUG", "INFO", "ERROR"):
|
|
706
|
+
return normalized
|
|
707
|
+
|
|
708
|
+
return LOGGING_TO_LEVEL_LABEL.get(
|
|
709
|
+
normalized_level,
|
|
710
|
+
str(logging.getLevelName(normalized_level)),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
def setLevel(self, level: Union[LEVEL, int, str]) -> None:
|
|
714
|
+
"""Set the per-instance minimum output threshold for LogBar methods."""
|
|
715
|
+
|
|
716
|
+
super().setLevel(self._normalize_level(level))
|
|
717
|
+
|
|
630
718
|
def columns(self, *headers, cols: Optional[Sequence] = None, width: Optional[Union[str, int, float]] = None, padding: int = 2):
|
|
631
719
|
"""Return a column-aware helper that keeps column widths aligned."""
|
|
632
720
|
|
|
@@ -707,9 +795,15 @@ class LogBar(logging.Logger):
|
|
|
707
795
|
|
|
708
796
|
return " ".join(part for part in parts if part)
|
|
709
797
|
|
|
710
|
-
def _process(self, level: LEVEL, msg, *args, **kwargs):
|
|
798
|
+
def _process(self, level: Union[LEVEL, int, str], msg, *args, **kwargs):
|
|
711
799
|
global last_rendered_length
|
|
712
800
|
|
|
801
|
+
normalized_level = self._normalize_level(level)
|
|
802
|
+
if not self.isEnabledFor(normalized_level):
|
|
803
|
+
return
|
|
804
|
+
|
|
805
|
+
level_label = self._level_label(level, normalized_level)
|
|
806
|
+
level_width = max(LEVEL_MAX_LENGTH, len(level_label))
|
|
713
807
|
str_msg = self._format_message(msg, args)
|
|
714
808
|
|
|
715
809
|
with _RENDER_LOCK:
|
|
@@ -718,10 +812,10 @@ class LogBar(logging.Logger):
|
|
|
718
812
|
with _STATE_LOCK:
|
|
719
813
|
previous_render_length = last_rendered_length
|
|
720
814
|
|
|
721
|
-
line_length =
|
|
815
|
+
line_length = level_width + 1 + len(str_msg)
|
|
722
816
|
|
|
723
817
|
if columns > 0:
|
|
724
|
-
padding_needed = max(0, columns -
|
|
818
|
+
padding_needed = max(0, columns - level_width - 2 - len(str_msg))
|
|
725
819
|
rendered_message = f"{str_msg}{' ' * padding_needed}"
|
|
726
820
|
printable_length = columns
|
|
727
821
|
else:
|
|
@@ -732,10 +826,10 @@ class LogBar(logging.Logger):
|
|
|
732
826
|
_clear_progress_stack_locked(for_log_output=True)
|
|
733
827
|
|
|
734
828
|
reset = COLORS["RESET"]
|
|
735
|
-
color = COLORS.get(
|
|
829
|
+
color = COLORS.get(level_label, reset)
|
|
736
830
|
|
|
737
|
-
level_padding = " " * (
|
|
738
|
-
_print(f"\r{color}{
|
|
831
|
+
level_padding = " " * (level_width - len(level_label))
|
|
832
|
+
_print(f"\r{color}{level_label}{reset}{level_padding} {rendered_message}", end='\n', flush=True)
|
|
739
833
|
|
|
740
834
|
with _STATE_LOCK:
|
|
741
835
|
last_rendered_length = printable_length
|
|
@@ -695,7 +695,10 @@ class ProgressBar:
|
|
|
695
695
|
def calc_time(self, iteration):
|
|
696
696
|
used_time = int(time.time() - self.time)
|
|
697
697
|
formatted_time = str(datetime.timedelta(seconds=used_time))
|
|
698
|
-
|
|
698
|
+
completed = iteration
|
|
699
|
+
if self._render_mode == RenderMode.AUTO and iteration > 0:
|
|
700
|
+
completed = iteration - 1
|
|
701
|
+
remaining = str(datetime.timedelta(seconds=int((used_time / max(completed, 1)) * len(self))))
|
|
699
702
|
return f"{formatted_time} / {remaining}"
|
|
700
703
|
|
|
701
704
|
def _render_snapshot(self, columns: Optional[int] = None) -> str:
|
|
@@ -110,6 +110,8 @@ def test_columns_simulate_updates_width_without_output():
|
|
|
110
110
|
|
|
111
111
|
def test_columns_support_other_levels(capsys):
|
|
112
112
|
cols = log.columns(cols=("name", "age"))
|
|
113
|
+
previous_level = log.level
|
|
114
|
+
log.setLevel("DEBUG")
|
|
113
115
|
|
|
114
116
|
buffer = io.StringIO()
|
|
115
117
|
|
|
@@ -123,17 +125,20 @@ def test_columns_support_other_levels(capsys):
|
|
|
123
125
|
sys.__stdout__.flush()
|
|
124
126
|
buffer.flush()
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
with
|
|
128
|
-
with
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
try:
|
|
129
|
+
with mock.patch('logbar.logbar.terminal_size', return_value=(0, 0)):
|
|
130
|
+
with capsys.disabled():
|
|
131
|
+
with redirect_stdout(Tee()):
|
|
132
|
+
cols.debug.header()
|
|
133
|
+
cols.debug("debug", "10")
|
|
134
|
+
cols.warn.header()
|
|
135
|
+
cols.warn("warn", "20")
|
|
136
|
+
cols.error.header()
|
|
137
|
+
cols.error("error", "30")
|
|
138
|
+
cols.critical.header()
|
|
139
|
+
cols.critical("critical", "40")
|
|
140
|
+
finally:
|
|
141
|
+
log.setLevel(previous_level)
|
|
137
142
|
|
|
138
143
|
captured = _clean(buffer.getvalue())
|
|
139
144
|
lines = [line for line in captured.splitlines() if line.strip()]
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
5
|
|
|
6
6
|
import io
|
|
7
|
+
import logging
|
|
7
8
|
import sys
|
|
8
9
|
import threading
|
|
9
10
|
from contextlib import redirect_stdout
|
|
@@ -39,6 +40,50 @@ class TestProgressBar(unittest.TestCase):
|
|
|
39
40
|
log.error("hello error")
|
|
40
41
|
log.critical("hello critical")
|
|
41
42
|
|
|
43
|
+
def test_set_level_string_filters_custom_helpers(self):
|
|
44
|
+
local_log = LogBar("test_set_level_string_filters_custom_helpers")
|
|
45
|
+
local_log.setLevel("ERROR")
|
|
46
|
+
|
|
47
|
+
def emit():
|
|
48
|
+
local_log.debug("debug hidden token")
|
|
49
|
+
local_log.info("info hidden token")
|
|
50
|
+
local_log.error("error visible token")
|
|
51
|
+
|
|
52
|
+
output = self.capture_log(emit)
|
|
53
|
+
self.assertIn("error visible token", output)
|
|
54
|
+
self.assertNotIn("debug hidden token", output)
|
|
55
|
+
self.assertNotIn("info hidden token", output)
|
|
56
|
+
|
|
57
|
+
def test_set_level_filters_custom_helpers(self):
|
|
58
|
+
local_log = LogBar("test_set_level_filters_custom_helpers")
|
|
59
|
+
local_log.setLevel(logging.WARNING)
|
|
60
|
+
|
|
61
|
+
def emit():
|
|
62
|
+
local_log.info("info hidden by setLevel")
|
|
63
|
+
local_log.warn("warn visible by setLevel")
|
|
64
|
+
|
|
65
|
+
output = self.capture_log(emit)
|
|
66
|
+
self.assertIn("warn visible by setLevel", output)
|
|
67
|
+
self.assertNotIn("info hidden by setLevel", output)
|
|
68
|
+
|
|
69
|
+
def test_set_level_logbar_warning_alias_filters_custom_helpers(self):
|
|
70
|
+
local_log = LogBar("test_set_level_logbar_warning_alias_filters_custom_helpers")
|
|
71
|
+
self.assertEqual(LogBar.WARNING, logging.WARNING)
|
|
72
|
+
local_log.setLevel(LogBar.WARNING)
|
|
73
|
+
|
|
74
|
+
def emit():
|
|
75
|
+
local_log.info("info hidden by LogBar.WARNING")
|
|
76
|
+
local_log.warn("warn visible by LogBar.WARNING")
|
|
77
|
+
|
|
78
|
+
output = self.capture_log(emit)
|
|
79
|
+
self.assertIn("warn visible by LogBar.WARNING", output)
|
|
80
|
+
self.assertNotIn("info hidden by LogBar.WARNING", output)
|
|
81
|
+
|
|
82
|
+
def test_set_level_rejects_unknown_names(self):
|
|
83
|
+
local_log = LogBar("test_set_level_rejects_unknown_names")
|
|
84
|
+
with self.assertRaises(ValueError):
|
|
85
|
+
local_log.setLevel("TRACE")
|
|
86
|
+
|
|
42
87
|
def test_log_without_terminal_state(self):
|
|
43
88
|
"""LogBar should operate even when the runtime lacks a terminal."""
|
|
44
89
|
|
|
@@ -173,6 +173,31 @@ class TestProgress(unittest.TestCase):
|
|
|
173
173
|
with redirect_stdout(StringIO()):
|
|
174
174
|
pb.close()
|
|
175
175
|
|
|
176
|
+
def test_time_estimate_accounts_for_last_item(self):
|
|
177
|
+
pb = ProgressBar(range(3))
|
|
178
|
+
pb.current_iter_step = 3
|
|
179
|
+
pb.time = 0
|
|
180
|
+
|
|
181
|
+
with patch('logbar.progress.time.time', return_value=10):
|
|
182
|
+
estimate = pb.calc_time(pb.step())
|
|
183
|
+
|
|
184
|
+
elapsed_str, total_str = [part.strip() for part in estimate.split("/", 1)]
|
|
185
|
+
|
|
186
|
+
def to_seconds(value: str) -> int:
|
|
187
|
+
parts = [int(part) for part in value.split(":")]
|
|
188
|
+
if len(parts) == 3:
|
|
189
|
+
hours, minutes, seconds = parts
|
|
190
|
+
elif len(parts) == 2:
|
|
191
|
+
hours = 0
|
|
192
|
+
minutes, seconds = parts
|
|
193
|
+
else:
|
|
194
|
+
hours = 0
|
|
195
|
+
minutes = 0
|
|
196
|
+
seconds = parts[0]
|
|
197
|
+
return hours * 3600 + minutes * 60 + seconds
|
|
198
|
+
|
|
199
|
+
self.assertGreater(to_seconds(total_str), to_seconds(elapsed_str))
|
|
200
|
+
|
|
176
201
|
def test_draw_without_terminal_state(self):
|
|
177
202
|
pb = log.pb(10).manual()
|
|
178
203
|
pb.current_iter_step = 5
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|