logbar 0.2.2__tar.gz → 0.2.4__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.2 → logbar-0.2.4}/LogBar.egg-info/PKG-INFO +14 -2
- {logbar-0.2.2 → logbar-0.2.4}/PKG-INFO +14 -2
- {logbar-0.2.2 → logbar-0.2.4}/README.md +13 -1
- {logbar-0.2.2 → logbar-0.2.4}/logbar/logbar.py +13 -3
- {logbar-0.2.2 → logbar-0.2.4}/logbar/progress.py +104 -4
- {logbar-0.2.2 → logbar-0.2.4}/pyproject.toml +1 -1
- {logbar-0.2.2 → logbar-0.2.4}/tests/test_progress.py +106 -0
- {logbar-0.2.2 → logbar-0.2.4}/LICENSE +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/LogBar.egg-info/SOURCES.txt +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/LogBar.egg-info/dependency_links.txt +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/LogBar.egg-info/top_level.txt +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/logbar/__init__.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/logbar/buffer.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/logbar/columns.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/logbar/terminal.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/logbar/util.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/setup.cfg +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/tests/test_columns.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/tests/test_example.py +0 -0
- {logbar-0.2.2 → logbar-0.2.4}/tests/test_log.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.4
|
|
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
|
|
@@ -38,6 +38,9 @@ Dynamic: license-file
|
|
|
38
38
|
- Stackable progress bars that stay anchored while your logs flow freely.
|
|
39
39
|
- Built-in styling for progress bar fills, colors, gradients, and head glyphs.
|
|
40
40
|
- Animated progress titles with a subtle sweeping highlight.
|
|
41
|
+
Set `LOGBAR_ANIMATION=0` to disable the highlight animation.
|
|
42
|
+
- Progress output throttling for reducing redraw churn in batch-heavy jobs.
|
|
43
|
+
Set `LOGBAR_PROGRESS_OUTPUT_INTERVAL=10` to render every 10 logical updates instead of every update.
|
|
41
44
|
- Column-aware table printer with spans, width hints, and `fit` sizing.
|
|
42
45
|
- Zero dependencies; works anywhere Python runs.
|
|
43
46
|
|
|
@@ -113,6 +116,15 @@ for _ in log.pb(500).title("Downloading"):
|
|
|
113
116
|
time.sleep(0.05)
|
|
114
117
|
```
|
|
115
118
|
|
|
119
|
+
When a workload updates progress very frequently, throttle redraw churn globally or per bar:
|
|
120
|
+
|
|
121
|
+
```py
|
|
122
|
+
for _ in log.pb(500, output_interval=10).title("Quantizing"):
|
|
123
|
+
time.sleep(0.01)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`output_interval=10` means LogBar will emit a fresh snapshot after roughly every 10 logical progress steps, while still forcing the last pending step to render before the bar closes. Set `LOGBAR_PROGRESS_OUTPUT_INTERVAL=10` to apply the same default process-wide.
|
|
127
|
+
|
|
116
128
|
Manual mode gives full control when you need to interleave logging and redraws:
|
|
117
129
|
|
|
118
130
|
```py
|
|
@@ -143,7 +155,7 @@ with log.spinner("Loading model") as spinner:
|
|
|
143
155
|
warm_up()
|
|
144
156
|
```
|
|
145
157
|
|
|
146
|
-
The rolling bar animates automatically while attached. Close it explicitly with `spinner.close()` if you are not using the context manager.
|
|
158
|
+
The rolling bar animates automatically while attached. Close it explicitly with `spinner.close()` if you are not using the context manager. Set `LOGBAR_ANIMATION=0` to disable the title highlight sweep on progress labels.
|
|
147
159
|
|
|
148
160
|
### Multiple Progress Bars
|
|
149
161
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: LogBar
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -38,6 +38,9 @@ Dynamic: license-file
|
|
|
38
38
|
- Stackable progress bars that stay anchored while your logs flow freely.
|
|
39
39
|
- Built-in styling for progress bar fills, colors, gradients, and head glyphs.
|
|
40
40
|
- Animated progress titles with a subtle sweeping highlight.
|
|
41
|
+
Set `LOGBAR_ANIMATION=0` to disable the highlight animation.
|
|
42
|
+
- Progress output throttling for reducing redraw churn in batch-heavy jobs.
|
|
43
|
+
Set `LOGBAR_PROGRESS_OUTPUT_INTERVAL=10` to render every 10 logical updates instead of every update.
|
|
41
44
|
- Column-aware table printer with spans, width hints, and `fit` sizing.
|
|
42
45
|
- Zero dependencies; works anywhere Python runs.
|
|
43
46
|
|
|
@@ -113,6 +116,15 @@ for _ in log.pb(500).title("Downloading"):
|
|
|
113
116
|
time.sleep(0.05)
|
|
114
117
|
```
|
|
115
118
|
|
|
119
|
+
When a workload updates progress very frequently, throttle redraw churn globally or per bar:
|
|
120
|
+
|
|
121
|
+
```py
|
|
122
|
+
for _ in log.pb(500, output_interval=10).title("Quantizing"):
|
|
123
|
+
time.sleep(0.01)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`output_interval=10` means LogBar will emit a fresh snapshot after roughly every 10 logical progress steps, while still forcing the last pending step to render before the bar closes. Set `LOGBAR_PROGRESS_OUTPUT_INTERVAL=10` to apply the same default process-wide.
|
|
127
|
+
|
|
116
128
|
Manual mode gives full control when you need to interleave logging and redraws:
|
|
117
129
|
|
|
118
130
|
```py
|
|
@@ -143,7 +155,7 @@ with log.spinner("Loading model") as spinner:
|
|
|
143
155
|
warm_up()
|
|
144
156
|
```
|
|
145
157
|
|
|
146
|
-
The rolling bar animates automatically while attached. Close it explicitly with `spinner.close()` if you are not using the context manager.
|
|
158
|
+
The rolling bar animates automatically while attached. Close it explicitly with `spinner.close()` if you are not using the context manager. Set `LOGBAR_ANIMATION=0` to disable the title highlight sweep on progress labels.
|
|
147
159
|
|
|
148
160
|
### Multiple Progress Bars
|
|
149
161
|
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
- Stackable progress bars that stay anchored while your logs flow freely.
|
|
24
24
|
- Built-in styling for progress bar fills, colors, gradients, and head glyphs.
|
|
25
25
|
- Animated progress titles with a subtle sweeping highlight.
|
|
26
|
+
Set `LOGBAR_ANIMATION=0` to disable the highlight animation.
|
|
27
|
+
- Progress output throttling for reducing redraw churn in batch-heavy jobs.
|
|
28
|
+
Set `LOGBAR_PROGRESS_OUTPUT_INTERVAL=10` to render every 10 logical updates instead of every update.
|
|
26
29
|
- Column-aware table printer with spans, width hints, and `fit` sizing.
|
|
27
30
|
- Zero dependencies; works anywhere Python runs.
|
|
28
31
|
|
|
@@ -98,6 +101,15 @@ for _ in log.pb(500).title("Downloading"):
|
|
|
98
101
|
time.sleep(0.05)
|
|
99
102
|
```
|
|
100
103
|
|
|
104
|
+
When a workload updates progress very frequently, throttle redraw churn globally or per bar:
|
|
105
|
+
|
|
106
|
+
```py
|
|
107
|
+
for _ in log.pb(500, output_interval=10).title("Quantizing"):
|
|
108
|
+
time.sleep(0.01)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`output_interval=10` means LogBar will emit a fresh snapshot after roughly every 10 logical progress steps, while still forcing the last pending step to render before the bar closes. Set `LOGBAR_PROGRESS_OUTPUT_INTERVAL=10` to apply the same default process-wide.
|
|
112
|
+
|
|
101
113
|
Manual mode gives full control when you need to interleave logging and redraws:
|
|
102
114
|
|
|
103
115
|
```py
|
|
@@ -128,7 +140,7 @@ with log.spinner("Loading model") as spinner:
|
|
|
128
140
|
warm_up()
|
|
129
141
|
```
|
|
130
142
|
|
|
131
|
-
The rolling bar animates automatically while attached. Close it explicitly with `spinner.close()` if you are not using the context manager.
|
|
143
|
+
The rolling bar animates automatically while attached. Close it explicitly with `spinner.close()` if you are not using the context manager. Set `LOGBAR_ANIMATION=0` to disable the title highlight sweep on progress labels.
|
|
132
144
|
|
|
133
145
|
### Multiple Progress Bars
|
|
134
146
|
|
|
@@ -319,7 +319,17 @@ def _render_progress_stack_locked(precomputed: Optional[dict] = None, columns_hi
|
|
|
319
319
|
|
|
320
320
|
if rendered is None:
|
|
321
321
|
try:
|
|
322
|
-
|
|
322
|
+
# Progress bars may throttle redraws. When a bar is not due for
|
|
323
|
+
# a fresh snapshot, keep its last rendered line in the stack
|
|
324
|
+
# instead of recomputing it opportunistically on another bar's
|
|
325
|
+
# redraw.
|
|
326
|
+
resolve_rendered = getattr(pb, "_resolve_rendered_line", None)
|
|
327
|
+
if callable(resolve_rendered):
|
|
328
|
+
rendered = resolve_rendered(columns)
|
|
329
|
+
if rendered is None:
|
|
330
|
+
rendered = pb._last_rendered_line or ""
|
|
331
|
+
else:
|
|
332
|
+
rendered = pb._render_snapshot(columns)
|
|
323
333
|
except Exception: # pragma: no cover - avoid breaking logging on render issues
|
|
324
334
|
rendered = pb._last_rendered_line or ""
|
|
325
335
|
else:
|
|
@@ -565,10 +575,10 @@ class LogBar(logging.Logger):
|
|
|
565
575
|
return shared_logger
|
|
566
576
|
|
|
567
577
|
|
|
568
|
-
def pb(self, iterable: Iterable):
|
|
578
|
+
def pb(self, iterable: Iterable, *, output_interval: Optional[int] = None):
|
|
569
579
|
from logbar.progress import ProgressBar
|
|
570
580
|
|
|
571
|
-
return ProgressBar(iterable, owner=self).attach(self)
|
|
581
|
+
return ProgressBar(iterable, owner=self, output_interval=output_interval).attach(self)
|
|
572
582
|
|
|
573
583
|
def spinner(self, title: str = "", *, interval: float = 0.5, tail_length: int = 4):
|
|
574
584
|
from logbar.progress import RollingProgressBar
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
5
|
|
|
6
6
|
import datetime
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
8
9
|
import sys
|
|
9
10
|
import time
|
|
@@ -11,6 +12,7 @@ import threading
|
|
|
11
12
|
from contextlib import contextmanager, nullcontext
|
|
12
13
|
from dataclasses import dataclass, replace
|
|
13
14
|
from enum import Enum
|
|
15
|
+
from functools import lru_cache
|
|
14
16
|
from typing import Iterable, Optional, Union, TYPE_CHECKING, Sequence
|
|
15
17
|
from warnings import warn
|
|
16
18
|
|
|
@@ -74,6 +76,33 @@ def _iter_ansi_tokens(text: str):
|
|
|
74
76
|
i += 1
|
|
75
77
|
|
|
76
78
|
|
|
79
|
+
@lru_cache(maxsize=1)
|
|
80
|
+
def _env_animation_enabled() -> bool:
|
|
81
|
+
value = os.environ.get("LOGBAR_ANIMATION", "1")
|
|
82
|
+
return str(value).strip().lower() not in {"0", "false", "off", "no"}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _normalize_output_interval(value: Optional[Union[str, int]]) -> int:
|
|
86
|
+
"""Normalize logical progress render intervals to a safe integer >= 1."""
|
|
87
|
+
|
|
88
|
+
if value is None:
|
|
89
|
+
return 1
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
normalized = int(str(value).strip())
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
return 1
|
|
95
|
+
|
|
96
|
+
return max(1, normalized)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@lru_cache(maxsize=1)
|
|
100
|
+
def _env_progress_output_interval() -> int:
|
|
101
|
+
"""Global default for how many logical progress updates occur per render."""
|
|
102
|
+
|
|
103
|
+
return _normalize_output_interval(os.environ.get("LOGBAR_PROGRESS_OUTPUT_INTERVAL", "1"))
|
|
104
|
+
|
|
105
|
+
|
|
77
106
|
def _fg_256(code: int) -> str:
|
|
78
107
|
return f"\033[38;5;{code}m"
|
|
79
108
|
|
|
@@ -366,7 +395,13 @@ class ProgressBar:
|
|
|
366
395
|
def default_style(cls) -> ProgressStyle:
|
|
367
396
|
return get_progress_style(_DEFAULT_STYLE_NAME)
|
|
368
397
|
|
|
369
|
-
def __init__(
|
|
398
|
+
def __init__(
|
|
399
|
+
self,
|
|
400
|
+
iterable: Union[Iterable, int, dict, set],
|
|
401
|
+
owner: Optional["LogBarType"] = None,
|
|
402
|
+
*,
|
|
403
|
+
output_interval: Optional[int] = None,
|
|
404
|
+
):
|
|
370
405
|
self._iterating = False # state: in init or active iteration
|
|
371
406
|
|
|
372
407
|
self._render_mode = RenderMode.AUTO
|
|
@@ -390,6 +425,10 @@ class ProgressBar:
|
|
|
390
425
|
self.time = time.time()
|
|
391
426
|
self._title_animation_start = self.time
|
|
392
427
|
self._title_animation_period = 0.1
|
|
428
|
+
self._output_interval = _normalize_output_interval(
|
|
429
|
+
_env_progress_output_interval() if output_interval is None else output_interval
|
|
430
|
+
)
|
|
431
|
+
self._last_output_step: Optional[int] = None
|
|
393
432
|
|
|
394
433
|
self.ui_show_left_steps = True # show [1 of 100] on left side
|
|
395
434
|
self.ui_show_left_steps_offset = 0
|
|
@@ -416,6 +455,12 @@ class ProgressBar:
|
|
|
416
455
|
self._style_name = resolved.name
|
|
417
456
|
return self
|
|
418
457
|
|
|
458
|
+
def output_interval(self, interval: int):
|
|
459
|
+
"""Render after at least `interval` logical progress updates."""
|
|
460
|
+
|
|
461
|
+
self._output_interval = _normalize_output_interval(interval)
|
|
462
|
+
return self
|
|
463
|
+
|
|
419
464
|
def fill(self, fill: Union[str, ProgressStyle] = "█", empty: Optional[str] = None):
|
|
420
465
|
if isinstance(fill, ProgressStyle) or (isinstance(fill, str) and fill in _PROGRESS_STYLES):
|
|
421
466
|
return self.style(fill)
|
|
@@ -664,10 +709,50 @@ class ProgressBar:
|
|
|
664
709
|
|
|
665
710
|
return self
|
|
666
711
|
|
|
667
|
-
def
|
|
712
|
+
def _render_position(self) -> int:
|
|
713
|
+
"""Progress coordinate used for render throttling."""
|
|
714
|
+
|
|
715
|
+
return self.step()
|
|
716
|
+
|
|
717
|
+
def _should_render(self, force: bool = False, allow_repeat: bool = False) -> bool:
|
|
718
|
+
if force:
|
|
719
|
+
return True
|
|
720
|
+
|
|
721
|
+
if not self._last_rendered_line:
|
|
722
|
+
return True
|
|
723
|
+
|
|
724
|
+
last_output_step = self._last_output_step
|
|
725
|
+
if last_output_step is None:
|
|
726
|
+
return True
|
|
727
|
+
|
|
728
|
+
current_step = self._render_position()
|
|
729
|
+
if allow_repeat and current_step <= last_output_step:
|
|
730
|
+
return True
|
|
731
|
+
|
|
732
|
+
return (current_step - last_output_step) >= self._output_interval
|
|
733
|
+
|
|
734
|
+
def _resolve_rendered_line(
|
|
735
|
+
self,
|
|
736
|
+
columns: int,
|
|
737
|
+
force: bool = False,
|
|
738
|
+
allow_repeat: bool = False,
|
|
739
|
+
) -> Optional[str]:
|
|
740
|
+
if not self._should_render(force=force, allow_repeat=allow_repeat):
|
|
741
|
+
return None
|
|
742
|
+
|
|
743
|
+
rendered_line = self._render_snapshot(columns)
|
|
744
|
+
self._last_output_step = self._render_position()
|
|
745
|
+
return rendered_line
|
|
746
|
+
|
|
747
|
+
def draw(self, force: bool = False):
|
|
748
|
+
# Even skipped draws count as activity. This prevents the background
|
|
749
|
+
# refresher from redrawing the same bar immediately and undoing the
|
|
750
|
+
# throttle when progress is advancing quickly.
|
|
668
751
|
_record_progress_activity()
|
|
669
752
|
columns, _ = terminal_size()
|
|
670
|
-
rendered_line = self.
|
|
753
|
+
rendered_line = self._resolve_rendered_line(columns, force=force, allow_repeat=True)
|
|
754
|
+
if rendered_line is None:
|
|
755
|
+
return
|
|
671
756
|
|
|
672
757
|
render_fn = render_progress_stack if callable(render_progress_stack) else None
|
|
673
758
|
context, lock_held = self._render_lock_context()
|
|
@@ -878,6 +963,8 @@ class ProgressBar:
|
|
|
878
963
|
return ''.join(result)
|
|
879
964
|
|
|
880
965
|
def _should_animate_title(self) -> bool:
|
|
966
|
+
if not _env_animation_enabled():
|
|
967
|
+
return False
|
|
881
968
|
isatty = getattr(sys.stdout, "isatty", None)
|
|
882
969
|
if not callable(isatty):
|
|
883
970
|
return False
|
|
@@ -966,6 +1053,13 @@ class ProgressBar:
|
|
|
966
1053
|
if self.closed:
|
|
967
1054
|
return
|
|
968
1055
|
|
|
1056
|
+
# Force the last observed progress state to render even when the final
|
|
1057
|
+
# step does not land on the configured output interval, e.g. 15 with an
|
|
1058
|
+
# interval of 10. Without this, the closing detach would erase the bar
|
|
1059
|
+
# before a 100% snapshot is ever emitted.
|
|
1060
|
+
if self.step() > 0 and (self._last_output_step is None or self.step() != self._last_output_step):
|
|
1061
|
+
self.draw(force=True)
|
|
1062
|
+
|
|
969
1063
|
self.closed = True
|
|
970
1064
|
self.detach()
|
|
971
1065
|
|
|
@@ -974,7 +1068,10 @@ class RollingProgressBar(ProgressBar):
|
|
|
974
1068
|
"""Indeterminate progress indicator with a rolling highlight."""
|
|
975
1069
|
|
|
976
1070
|
def __init__(self, owner: Optional["LogBarType"] = None, interval: float = 0.5, tail_length: int = 4):
|
|
977
|
-
|
|
1071
|
+
# Spinner animation already has its own wall-clock interval. Keep
|
|
1072
|
+
# output throttling at 1 so the phase animation stays smooth unless a
|
|
1073
|
+
# caller explicitly changes it later.
|
|
1074
|
+
super().__init__(iterable=1, owner=owner, output_interval=1)
|
|
978
1075
|
self._interval = max(0.05, float(interval))
|
|
979
1076
|
self._tail_length = max(1, int(tail_length))
|
|
980
1077
|
self._phase = 0
|
|
@@ -1040,6 +1137,9 @@ class RollingProgressBar(ProgressBar):
|
|
|
1040
1137
|
def _advance_phase(self) -> None:
|
|
1041
1138
|
self._phase = (self._phase + 1) % 1_000_000
|
|
1042
1139
|
|
|
1140
|
+
def _render_position(self) -> int:
|
|
1141
|
+
return self._phase
|
|
1142
|
+
|
|
1043
1143
|
def _render_snapshot(self, columns: Optional[int] = None) -> str:
|
|
1044
1144
|
if columns is None:
|
|
1045
1145
|
columns, _ = terminal_size()
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
# Contact: qubitium@modelcloud.ai, x.com/qubitium
|
|
5
5
|
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
6
8
|
import random
|
|
7
9
|
import re
|
|
8
10
|
import sys
|
|
@@ -14,6 +16,7 @@ from time import sleep
|
|
|
14
16
|
from unittest.mock import patch
|
|
15
17
|
|
|
16
18
|
from logbar import LogBar
|
|
19
|
+
from logbar import progress as progress_module
|
|
17
20
|
from logbar.progress import ProgressBar, TITLE_HIGHLIGHT_COLOR, ANSI_BOLD_RESET
|
|
18
21
|
from logbar.logbar import _active_progress_bars
|
|
19
22
|
|
|
@@ -67,6 +70,33 @@ SAMPLES = generate_expanding_str_a_to_z()
|
|
|
67
70
|
REVERSED_SAMPLES = reversed(SAMPLES)
|
|
68
71
|
|
|
69
72
|
class TestProgress(unittest.TestCase):
|
|
73
|
+
def setUp(self):
|
|
74
|
+
self._saved_env = {
|
|
75
|
+
"LOGBAR_ANIMATION": os.environ.get("LOGBAR_ANIMATION"),
|
|
76
|
+
"LOGBAR_PROGRESS_OUTPUT_INTERVAL": os.environ.get("LOGBAR_PROGRESS_OUTPUT_INTERVAL"),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for key in self._saved_env:
|
|
80
|
+
os.environ.pop(key, None)
|
|
81
|
+
|
|
82
|
+
self._clear_progress_env_caches()
|
|
83
|
+
|
|
84
|
+
def tearDown(self):
|
|
85
|
+
for key, value in self._saved_env.items():
|
|
86
|
+
if value is None:
|
|
87
|
+
os.environ.pop(key, None)
|
|
88
|
+
else:
|
|
89
|
+
os.environ[key] = value
|
|
90
|
+
|
|
91
|
+
self._clear_progress_env_caches()
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _clear_progress_env_caches():
|
|
95
|
+
for helper_name in ("_env_animation_enabled", "_env_progress_output_interval"):
|
|
96
|
+
helper = getattr(progress_module, helper_name, None)
|
|
97
|
+
cache_clear = getattr(helper, "cache_clear", None)
|
|
98
|
+
if callable(cache_clear):
|
|
99
|
+
cache_clear()
|
|
70
100
|
|
|
71
101
|
def test_title_fixed_subtitle_dynamic(self):
|
|
72
102
|
pb = log.pb(SAMPLES).title("TITLE:").manual()
|
|
@@ -139,6 +169,82 @@ class TestProgress(unittest.TestCase):
|
|
|
139
169
|
with redirect_stdout(StringIO()):
|
|
140
170
|
pb.close()
|
|
141
171
|
|
|
172
|
+
def test_title_animation_respects_logbar_animation_env(self):
|
|
173
|
+
script = (
|
|
174
|
+
"from types import SimpleNamespace\n"
|
|
175
|
+
"import sys\n"
|
|
176
|
+
"from logbar.progress import ProgressBar\n"
|
|
177
|
+
"pb = ProgressBar(range(1)).manual()\n"
|
|
178
|
+
"sys.stdout = SimpleNamespace(isatty=lambda: True, write=lambda *_: None, flush=lambda: None)\n"
|
|
179
|
+
"sys.__stdout__.write('1\\n' if pb._should_animate_title() else '0\\n')\n"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
enabled = subprocess.run(
|
|
183
|
+
[sys.executable, "-c", script],
|
|
184
|
+
cwd="/root/LogBar",
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
env={},
|
|
188
|
+
check=True,
|
|
189
|
+
)
|
|
190
|
+
self.assertEqual(enabled.stdout.strip(), "1")
|
|
191
|
+
|
|
192
|
+
disabled = subprocess.run(
|
|
193
|
+
[sys.executable, "-c", script],
|
|
194
|
+
cwd="/root/LogBar",
|
|
195
|
+
capture_output=True,
|
|
196
|
+
text=True,
|
|
197
|
+
env={"LOGBAR_ANIMATION": "0"},
|
|
198
|
+
check=True,
|
|
199
|
+
)
|
|
200
|
+
self.assertEqual(disabled.stdout.strip(), "0")
|
|
201
|
+
|
|
202
|
+
def test_progress_output_interval_respects_env(self):
|
|
203
|
+
cache_clear = progress_module._env_progress_output_interval.cache_clear
|
|
204
|
+
|
|
205
|
+
with patch.dict(os.environ, {"LOGBAR_PROGRESS_OUTPUT_INTERVAL": "10"}):
|
|
206
|
+
cache_clear()
|
|
207
|
+
pb = ProgressBar(range(5))
|
|
208
|
+
self.assertEqual(pb._output_interval, 10)
|
|
209
|
+
with redirect_stdout(StringIO()):
|
|
210
|
+
pb.close()
|
|
211
|
+
|
|
212
|
+
cache_clear()
|
|
213
|
+
|
|
214
|
+
def test_progress_output_interval_defaults_to_one(self):
|
|
215
|
+
cache_clear = progress_module._env_progress_output_interval.cache_clear
|
|
216
|
+
|
|
217
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
218
|
+
cache_clear()
|
|
219
|
+
pb = ProgressBar(range(5))
|
|
220
|
+
self.assertEqual(pb._output_interval, 1)
|
|
221
|
+
with redirect_stdout(StringIO()):
|
|
222
|
+
pb.close()
|
|
223
|
+
|
|
224
|
+
cache_clear()
|
|
225
|
+
|
|
226
|
+
def test_progress_output_interval_skips_intermediate_draws_but_flushes_final_step(self):
|
|
227
|
+
columns = 96
|
|
228
|
+
|
|
229
|
+
with patch('logbar.progress.terminal_size', return_value=(columns, 24)), \
|
|
230
|
+
patch('logbar.logbar.terminal_size', return_value=(columns, 24)):
|
|
231
|
+
buffer = StringIO()
|
|
232
|
+
with redirect_stdout(buffer):
|
|
233
|
+
pb = log.pb(15, output_interval=10).manual()
|
|
234
|
+
for step in range(1, 16):
|
|
235
|
+
pb.current_iter_step = step
|
|
236
|
+
pb.draw()
|
|
237
|
+
pb.close()
|
|
238
|
+
|
|
239
|
+
lines = extract_rendered_lines(buffer.getvalue())
|
|
240
|
+
progress_lines = [line for line in lines if "/15]" in line]
|
|
241
|
+
|
|
242
|
+
self.assertTrue(any("[0/15]" in line for line in progress_lines))
|
|
243
|
+
self.assertTrue(any("[10/15]" in line for line in progress_lines))
|
|
244
|
+
self.assertTrue(any("[15/15]" in line and "100.0%" in line for line in progress_lines))
|
|
245
|
+
self.assertFalse(any("[9/15]" in line for line in progress_lines))
|
|
246
|
+
self.assertFalse(any("[14/15]" in line for line in progress_lines))
|
|
247
|
+
|
|
142
248
|
def test_draw_respects_terminal_width(self):
|
|
143
249
|
pb = log.pb(100).title("TITLE").subtitle("SUBTITLE").manual()
|
|
144
250
|
pb.current_iter_step = 50
|
|
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
|
|
File without changes
|
|
File without changes
|