diff --git a/scripts/download.py b/scripts/download.py index f9b603ff..1097e295 100644 --- a/scripts/download.py +++ b/scripts/download.py @@ -194,9 +194,13 @@ def download_file( url, headers={"User-Agent": "retrobios-downloader/1.0"} ) + # A redrawn bar only on a terminal: written to a log, 32,768 redraws of a + # 2 GiB part made one line of nearly 3 MB. + live = sys.stdout.isatty() and os.environ.get("TERM") != "dumb" with urllib.request.urlopen(req, timeout=300) as resp: total = int(resp.headers.get("Content-Length", expected_size)) downloaded = 0 + shown = -1 with open(dest, "wb") as f: while True: @@ -206,16 +210,21 @@ def download_file( f.write(chunk) downloaded += len(chunk) - if total > 0: + if total > 0 and live: pct = downloaded * 100 // total - bar = "=" * (pct // 2) + " " * (50 - pct // 2) - print( - f"\r {label}[{bar}] {pct}% ({downloaded:,}/{total:,})", - end="", - flush=True, - ) + if pct != shown: + shown = pct + bar = "=" * (pct // 2) + " " * (50 - pct // 2) + print( + f"\r {label}[{bar}] {pct}% ({downloaded:,}/{total:,})", + end="", + flush=True, + ) - print() + if live: + print() + else: + print(f" {label}{downloaded:,} bytes") if expected_size and downloaded != expected_size: raise ValueError(f"downloaded {downloaded} bytes; expected {expected_size}") diff --git a/tests/test_download.py b/tests/test_download.py index 3a6fbbad..0702b2c2 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -583,5 +583,39 @@ class TestZipPartsShell(ZipPartsTests, ZipPartsCase, TestDownloadShell): pass + +class AProgressBarOnlyOnATerminal(unittest.TestCase): + """Redirected to a log, download.py wrote a carriage-return redraw per + 64 KiB block: one line of nearly 3 MB for a 2 GiB part.""" + + def test_a_log_gets_one_line(self): + import contextlib + import io as _io + import tempfile as _tempfile + from pathlib import Path as _Path + from unittest import mock + + import download + + class Body(_io.BytesIO): + headers = {"Content-Length": str(200_000)} + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + out = _io.StringIO() + with _tempfile.TemporaryDirectory() as directory, \ + mock.patch.object(download.urllib.request, "urlopen", + lambda *a, **k: Body(b"x" * 200_000)), \ + contextlib.redirect_stdout(out): + download.download_file("https://example.com/a.zip", + _Path(directory) / "a.zip", 200_000, "a.zip ") + self.assertNotIn("\r", out.getvalue()) + self.assertEqual(out.getvalue().count("\n"), 1) + + if __name__ == "__main__": unittest.main()