Coverage for tests / test_progress_bar.py: 100%

244 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-09 00:15 +0000

1""" 

2Tests for the Progress bar functionality. 

3Created after vendoring Click to ensure test coverage is back up to 100%. 

4""" 

5 

6import io 1cdabefg

7import shutil 1cdabefg

8 

9import pytest 1cdabefg

10import typer 1cdabefg

11from typer import progressbar 1cdabefg

12from typer._click import _termui_impl 1cdabefg

13from typer.testing import CliRunner 1cdabefg

14 

15runner = CliRunner() 1cdabefg

16 

17 

18def _fake_clock(monkeypatch: pytest.MonkeyPatch) -> list[float]: 1cdabefg

19 clock = [0.0] 1cdabefg

20 monkeypatch.setattr(_termui_impl.time, "time", lambda: clock[0]) 1cdabefg

21 return clock 1cdabefg

22 

23 

24def _pbar(**kw): 1cdabefg

25 return progressbar(file=kw.pop("file", io.StringIO()), **kw) 1cdabefg

26 

27 

28@pytest.mark.parametrize( 1cdabefg

29 ("iterable", "length", "hidden", "label", "expected_count"), 

30 [ 

31 (["a", "b"], None, False, "Processing", 2), 

32 (None, 3, False, "Counting", 3), 

33 (["x", "y"], None, True, "Hidden", 2), 

34 ], 

35) 

36def test_progressbar(iterable, length, hidden, label, expected_count): 1cdabefg

37 app = typer.Typer() 1cdabefg

38 

39 @app.command() 1cdabefg

40 def main(): 1cdabefg

41 bar_out = io.StringIO() 1cdabefg

42 count = 0 1cdabefg

43 with progressbar( 1cdabefg

44 iterable, length=length, hidden=hidden, label=label, file=bar_out 

45 ) as bar: 

46 for _ in bar: 1cdabefg

47 count += 1 1cdabefg

48 typer.echo(f"count={count}") 1cdabefg

49 typer.echo(f"bar={bar_out.getvalue()!r}") 1cdabefg

50 

51 result = runner.invoke(app, []) 1cdabefg

52 assert result.exit_code == 0, result.output 1cdabefg

53 assert f"count={expected_count}" in result.stdout 1cdabefg

54 assert (label in result.stdout) == (not hidden) 1cdabefg

55 

56 

57@pytest.mark.parametrize( 1cdabefg

58 ("label", "pbar_kw", "must_contain", "must_not_contain"), 

59 [ 

60 pytest.param( 

61 "TTY", 

62 { 

63 "show_pos": True, 

64 "show_percent": True, 

65 "item_show_func": lambda item: f"item={item}", 

66 }, 

67 ("TTY", "1/1", "100%", "item=x"), 

68 (), 

69 ), 

70 pytest.param( 

71 "HeurPct", 

72 {}, 

73 ("HeurPct", "100%"), 

74 ("1/1",), 

75 ), 

76 pytest.param( 

77 "HeurPos", 

78 {"show_pos": True}, 

79 ("HeurPos", "1/1"), 

80 ("100%",), 

81 ), 

82 ], 

83) 

84def test_progressbar_tty( 1cdabefg

85 monkeypatch, label: str, pbar_kw: dict, must_contain, must_not_contain 

86): 

87 monkeypatch.setattr(_termui_impl, "isatty", lambda f: True) 1cdabefg

88 _fake_clock(monkeypatch) 1cdabefg

89 

90 app = typer.Typer() 1cdabefg

91 

92 @app.command() 1cdabefg

93 def main(): 1cdabefg

94 bar_out = io.StringIO() 1cdabefg

95 with progressbar( 1cdabefg

96 ["x"], 

97 label=label, 

98 file=bar_out, 

99 bar_template="%(label)s %(info)s", 

100 width=1, 

101 **pbar_kw, 

102 ) as bar: 

103 for _ in bar: 1cdabefg

104 pass 1cdabefg

105 typer.echo(bar_out.getvalue()) 1cdabefg

106 

107 result = runner.invoke(app, []) 1cdabefg

108 assert result.exit_code == 0, result.output 1cdabefg

109 for part in must_contain: 1cdabefg

110 assert part in result.stdout 1cdabefg

111 for part in must_not_contain: 1cdabefg

112 assert part not in result.stdout 1cdabefg

113 

114 

115def test_progressbar_tty_show_eta(monkeypatch): 1cdabefg

116 monkeypatch.setattr(_termui_impl, "isatty", lambda f: True) 1cdabefg

117 clock = _fake_clock(monkeypatch) 1cdabefg

118 clock[0] = 1_000.0 1cdabefg

119 

120 app = typer.Typer() 1cdabefg

121 

122 @app.command() 1cdabefg

123 def main(): 1cdabefg

124 bar_out = io.StringIO() 1cdabefg

125 with progressbar( 1cdabefg

126 ["a", "b"], 

127 label="ETA", 

128 file=bar_out, 

129 show_pos=True, 

130 show_percent=False, 

131 show_eta=True, 

132 bar_template="%(label)s %(info)s", 

133 width=1, 

134 ) as bar: 

135 for i, _ in enumerate(bar): 1cdabefg

136 if i == 0: 1cdabefg

137 clock[0] = 1_001.0 1cdabefg

138 typer.echo(bar_out.getvalue()) 1cdabefg

139 

140 result = runner.invoke(app, []) 1cdabefg

141 assert result.exit_code == 0, result.output 1cdabefg

142 for part in ("ETA", "1/2", "00:00:01"): 1cdabefg

143 assert part in result.stdout 1cdabefg

144 

145 

146def test_progressbar_autowidth(monkeypatch): 1cdabefg

147 monkeypatch.setattr(_termui_impl, "isatty", lambda f: True) 1cdabefg

148 call = [0] 1cdabefg

149 real_get_terminal_size = shutil.get_terminal_size 1cdabefg

150 

151 def fake_get_terminal_size(*args, **kwargs): 1cdabefg

152 # Pytest (and others) call get_terminal_size(fallback=...); only stub no-arg calls 

153 if args or kwargs: 1cdabefg

154 return real_get_terminal_size(*args, **kwargs) 1ab

155 col = 120 if call[0] == 0 else 40 1cdabefg

156 call[0] += 1 1cdabefg

157 return type("TS", (), {"columns": col, "lines": 24})() 1cdabefg

158 

159 monkeypatch.setattr(shutil, "get_terminal_size", fake_get_terminal_size) 1cdabefg

160 

161 state: dict[str, object] = {} 1cdabefg

162 

163 app = typer.Typer() 1cdabefg

164 

165 @app.command() 1cdabefg

166 def main(): 1cdabefg

167 out = io.StringIO() 1cdabefg

168 with progressbar(["a", "b"], width=0, label="AW", file=out) as bar: 1cdabefg

169 state["autowidth"] = bar.autowidth 1cdabefg

170 for _ in bar: 1cdabefg

171 pass 1cdabefg

172 state["call_count"] = call[0] 1cdabefg

173 state["out"] = out.getvalue() 1cdabefg

174 typer.echo("done") 1cdabefg

175 

176 result = runner.invoke(app, []) 1cdabefg

177 assert result.exit_code == 0, result.output 1cdabefg

178 assert state["autowidth"] is True 1cdabefg

179 assert state["call_count"] >= 2 1cdabefg

180 out = str(state["out"]) 1cdabefg

181 assert "\r" in out and "AW" in out 1cdabefg

182 assert "0%" in out and "50%" in out and "100%" in out 1cdabefg

183 

184 

185def test_progress_bar_iter(): 1cdabefg

186 not_entered = _pbar(iterable=[1, 2], length=2) 1cdabefg

187 with pytest.raises(RuntimeError, match="with block"): 1cdabefg

188 iter(not_entered) 1cdabefg

189 

190 entered = _pbar(iterable=[10, 20], length=2) 1cdabefg

191 with entered: 1cdabefg

192 iterator = iter(entered) 1cdabefg

193 assert next(iterator) == 10 1cdabefg

194 assert next(entered) == 20 1cdabefg

195 with pytest.raises(StopIteration): 1cdabefg

196 next(iterator) 1cdabefg

197 

198 

199def test_progress_bar_time(monkeypatch): 1cdabefg

200 clock = _fake_clock(monkeypatch) 1cdabefg

201 state: dict[str, object] = {} 1cdabefg

202 clock[0] = 1_000.0 1cdabefg

203 

204 app = typer.Typer() 1cdabefg

205 

206 @app.command() 1cdabefg

207 def main(): 1cdabefg

208 bar = _pbar(iterable=None, length=10) 1cdabefg

209 state["tpi0"] = bar.time_per_iteration 1cdabefg

210 clock[0] = 1_000.5 1cdabefg

211 bar.make_step(1) 1cdabefg

212 state["avg_after_one"] = list(bar.avg) 1cdabefg

213 state["tpi_after_one"] = bar.time_per_iteration 1cdabefg

214 clock[0] = 1_001.0 1cdabefg

215 bar.make_step(1) 1cdabefg

216 state["pos2"] = bar.pos 1cdabefg

217 state["avg2"] = list(bar.avg) 1cdabefg

218 state["tpi2"] = bar.time_per_iteration 1cdabefg

219 clock[0] = 1_002.0 1cdabefg

220 bar.make_step(1) 1cdabefg

221 state["avg3"] = list(bar.avg) 1cdabefg

222 state["tpi3"] = bar.time_per_iteration 1cdabefg

223 typer.echo("ok") 1cdabefg

224 

225 result = runner.invoke(app, []) 1cdabefg

226 assert result.exit_code == 0, result.output 1cdabefg

227 assert state["tpi0"] == 0.0 1cdabefg

228 assert state["avg_after_one"] == [] and state["tpi_after_one"] == 0.0 1cdabefg

229 assert state["pos2"] == 2 1cdabefg

230 assert state["avg2"] == [(1_001.0 - 1_000.0) / 2.0] 1cdabefg

231 assert state["tpi2"] == pytest.approx(0.5) 1cdabefg

232 assert state["avg3"] == [0.5, (1_002.0 - 1_000.0) / 3.0] 1cdabefg

233 assert state["tpi3"] == pytest.approx(sum(state["avg3"]) / 2.0) # type: ignore[arg-type] 1cdabefg

234 

235 

236def test_progress_bar_time_zero_steps(monkeypatch): 1cdabefg

237 clock = _fake_clock(monkeypatch) 1cdabefg

238 state: dict[str, object] = {} 1cdabefg

239 clock[0] = 2_000.0 1cdabefg

240 

241 app = typer.Typer() 1cdabefg

242 

243 @app.command() 1cdabefg

244 def main(): 1cdabefg

245 bar = _pbar(iterable=None, length=3) 1cdabefg

246 clock[0] = 2_001.0 1cdabefg

247 bar.make_step(0) 1cdabefg

248 state["pos"] = bar.pos 1cdabefg

249 state["avg"] = list(bar.avg) 1cdabefg

250 state["tpi"] = bar.time_per_iteration 1cdabefg

251 typer.echo("ok") 1cdabefg

252 

253 result = runner.invoke(app, []) 1cdabefg

254 assert result.exit_code == 0, result.output 1cdabefg

255 assert state["pos"] == 0 1cdabefg

256 assert state["avg"] == [1.0] 1cdabefg

257 assert state["tpi"] == pytest.approx(1.0) 1cdabefg

258 

259 

260def test_progress_bar_eta(monkeypatch): 1cdabefg

261 state: dict[str, object] = {} 1cdabefg

262 

263 app = typer.Typer() 1cdabefg

264 

265 @app.command() 1cdabefg

266 def main(): 1cdabefg

267 state["eta0"] = _pbar(iterable=[1, 2], length=None).eta 1cdabefg

268 

269 done = _pbar(iterable=None, length=5) 1cdabefg

270 done.pos, done.finished = 2, True 1cdabefg

271 state["eta_done"] = done.eta 1cdabefg

272 

273 fresh = _pbar(iterable=None, length=5) 1cdabefg

274 state["eta_known_fresh"] = fresh.eta_known 1cdabefg

275 state["fmt_eta_fresh"] = fresh.format_eta() 1cdabefg

276 

277 clock = _fake_clock(monkeypatch) 1cdabefg

278 clock[0] = 5_000.0 1cdabefg

279 bar = _pbar(iterable=None, length=10) 1cdabefg

280 clock[0] = 5_001.0 1cdabefg

281 bar.make_step(3) 1cdabefg

282 state["pos3"] = bar.pos 1cdabefg

283 state["eta_after"] = bar.eta 1cdabefg

284 state["tpi"] = bar.time_per_iteration 1cdabefg

285 

286 cases_out = [] 1cdabefg

287 for t0, t1, length, n_steps, _expected_fmt, expected_eta_int in ( 1cdabefg

288 (9_000.0, 9_001.0, 10, 1, "00:00:09", None), 

289 (1_000.0, 100_000.0, 2, 1, "1d 03:30:00", 99_000), 

290 ): 

291 clock2 = _fake_clock(monkeypatch) 1cdabefg

292 clock2[0] = t0 1cdabefg

293 b = _pbar(iterable=None, length=length) 1cdabefg

294 clock2[0] = t1 1cdabefg

295 b.make_step(n_steps) 1cdabefg

296 cases_out.append( 1cdabefg

297 ( 

298 b.eta_known, 

299 b.format_eta(), 

300 int(b.eta) if expected_eta_int is not None else None, 

301 ) 

302 ) 

303 

304 state["cases"] = cases_out 1cdabefg

305 

306 clock3 = _fake_clock(monkeypatch) 1cdabefg

307 clock3[0] = 3_000.0 1cdabefg

308 b2 = _pbar(iterable=None, length=2) 1cdabefg

309 clock3[0] = 3_001.0 1cdabefg

310 b2.make_step(1) 1cdabefg

311 state["fmt_before_finish"] = b2.format_eta() 1cdabefg

312 b2.finish() 1cdabefg

313 state["fmt_after_finish"] = b2.format_eta() 1cdabefg

314 typer.echo("ok") 1cdabefg

315 

316 result = runner.invoke(app, []) 1cdabefg

317 assert result.exit_code == 0, result.output 1cdabefg

318 assert state["eta0"] == 0.0 1cdabefg

319 assert state["eta_done"] == 0.0 1cdabefg

320 assert not state["eta_known_fresh"] and state["fmt_eta_fresh"] == "" 1cdabefg

321 assert state["pos3"] == 3 1cdabefg

322 assert state["eta_after"] == pytest.approx(state["tpi"] * (10 - 3)) # type: ignore[operator] 1cdabefg

323 

324 (ek1, fmt1, ei1), (ek2, fmt2, ei2) = state["cases"] # type: ignore[misc] 1cdabefg

325 assert ek1 and fmt1 == "00:00:09" and ei1 is None 1cdabefg

326 assert ek2 and fmt2 == "1d 03:30:00" and ei2 == 99_000 1cdabefg

327 

328 assert state["fmt_before_finish"] != "" 1cdabefg

329 assert state["fmt_after_finish"] == "" 1cdabefg

330 

331 

332@pytest.mark.parametrize( 1cdabefg

333 ("width", "fill_char", "empty_char", "expected_bar", "finished", "sample_timing"), 

334 [ 

335 pytest.param(4, "X", "-", "XXXX", True, False, id="finished"), 

336 pytest.param(4, "#", "-", "----", False, False, id="no_timing_yet"), 

337 pytest.param(5, "*", ".", None, False, True, id="indeterminate"), 

338 ], 

339) 

340def test_progress_bar_unknown_length( 1cdabefg

341 monkeypatch, 

342 width: int, 

343 fill_char: str, 

344 empty_char: str, 

345 expected_bar: str | None, 

346 finished: bool, 

347 sample_timing: bool, 

348): 

349 clock: list[float] | None = _fake_clock(monkeypatch) if sample_timing else None 1cdabefg

350 if clock is not None: 1cdabefg

351 clock[0] = 100.0 1cdabefg

352 

353 state: dict[str, object] = {} 1cdabefg

354 

355 class _IterableWithoutLength: 1cdabefg

356 def __iter__(self): 1cdabefg

357 return iter((1, 2, 3)) 1cdabefg

358 

359 app = typer.Typer() 1cdabefg

360 

361 @app.command() 1cdabefg

362 def main(): 1cdabefg

363 bar = _pbar( 1cdabefg

364 iterable=_IterableWithoutLength(), 

365 length=None, 

366 width=width, 

367 fill_char=fill_char, 

368 empty_char=empty_char, 

369 ) 

370 assert bar.length is None 1cdabefg

371 

372 if sample_timing: 1cdabefg

373 assert clock is not None 1cdabefg

374 clock[0] = 101.0 1cdabefg

375 bar.make_step(1) 1cdabefg

376 assert bar.time_per_iteration > 0 1cdabefg

377 rendered = bar.format_bar() 1cdabefg

378 assert len(rendered) == width 1cdabefg

379 assert rendered.count(fill_char) == 1 1cdabefg

380 assert rendered.count(empty_char) == width - 1 1cdabefg

381 state["branch"] = "sample_timing" 1cdabefg

382 elif finished: 1cdabefg

383 bar.finished = True 1cdabefg

384 state["bar"] = bar.format_bar() 1cdabefg

385 state["branch"] = "finished" 1cdabefg

386 else: 

387 assert bar.time_per_iteration == 0.0 1cdabefg

388 state["bar"] = bar.format_bar() 1cdabefg

389 state["branch"] = "no_timing" 1cdabefg

390 typer.echo("ok") 1cdabefg

391 

392 result = runner.invoke(app, []) 1cdabefg

393 assert result.exit_code == 0, result.output 1cdabefg

394 if sample_timing: 1cdabefg

395 assert state["branch"] == "sample_timing" 1cdabefg

396 else: 

397 assert state["bar"] == expected_bar 1cdabefg