logbar 0.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: LogBar
3
- Version: 0.2.1
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.1
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 = len(level.value) + (LEVEL_MAX_LENGTH - len(level.value)) + 1 + len(str_msg)
815
+ line_length = level_width + 1 + len(str_msg)
722
816
 
723
817
  if columns > 0:
724
- padding_needed = max(0, columns - LEVEL_MAX_LENGTH - 2 - len(str_msg))
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(level.value, reset)
829
+ color = COLORS.get(level_label, reset)
736
830
 
737
- level_padding = " " * (LEVEL_MAX_LENGTH - len(level.value)) # 5 is max enum string length
738
- _print(f"\r{color}{level.value}{reset}{level_padding} {rendered_message}", end='\n', flush=True)
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "LogBar"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "A unified Logger and ProgressBar util with zero dependencies."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3"
@@ -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
- with mock.patch('logbar.logbar.terminal_size', return_value=(0, 0)):
127
- with capsys.disabled():
128
- with redirect_stdout(Tee()):
129
- cols.debug.header()
130
- cols.debug("debug", "10")
131
- cols.warn.header()
132
- cols.warn("warn", "20")
133
- cols.error.header()
134
- cols.error("error", "30")
135
- cols.critical.header()
136
- cols.critical("critical", "40")
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
 
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