Coverage for /Volumes/workspace/python-progressbar/.tox/py38/lib/python3.8/site-packages/progressbar/widgets.py: 99%

509 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-01 16:14 +0100

1# -*- coding: utf-8 -*- 

2from __future__ import annotations 

3 

4import abc 

5import datetime 

6import functools 

7import pprint 

8import sys 

9import typing 

10 

11from python_utils import converters, types 

12 

13from . import base, utils 

14 

15if types.TYPE_CHECKING: 

16 from .bar import ProgressBarMixinBase 

17 

18MAX_DATE = datetime.date.max 

19MAX_TIME = datetime.time.max 

20MAX_DATETIME = datetime.datetime.max 

21 

22Data = types.Dict[str, types.Any] 

23FormatString = typing.Optional[str] 

24 

25 

26def string_or_lambda(input_): 

27 if isinstance(input_, str): 

28 

29 def render_input(progress, data, width): 

30 return input_ % data 

31 

32 return render_input 

33 else: 

34 return input_ 

35 

36 

37def create_wrapper(wrapper): 

38 '''Convert a wrapper tuple or format string to a format string 

39 

40 >>> create_wrapper('') 

41 

42 >>> print(create_wrapper('a{}b')) 

43 a{}b 

44 

45 >>> print(create_wrapper(('a', 'b'))) 

46 a{}b 

47 ''' 

48 if isinstance(wrapper, tuple) and len(wrapper) == 2: 

49 a, b = wrapper 

50 wrapper = (a or '') + '{}' + (b or '') 

51 elif not wrapper: 

52 return 

53 

54 if isinstance(wrapper, str): 

55 assert '{}' in wrapper, 'Expected string with {} for formatting' 

56 else: 

57 raise RuntimeError( 

58 'Pass either a begin/end string as a tuple or a' 

59 ' template string with {}' 

60 ) 

61 

62 return wrapper 

63 

64 

65def wrapper(function, wrapper_): 

66 '''Wrap the output of a function in a template string or a tuple with 

67 begin/end strings 

68 

69 ''' 

70 wrapper_ = create_wrapper(wrapper_) 

71 if not wrapper_: 

72 return function 

73 

74 @functools.wraps(function) 

75 def wrap(*args, **kwargs): 

76 return wrapper_.format(function(*args, **kwargs)) 

77 

78 return wrap 

79 

80 

81def create_marker(marker, wrap=None): 

82 def _marker(progress, data, width): 

83 if ( 

84 progress.max_value is not base.UnknownLength 

85 and progress.max_value > 0 

86 ): 

87 length = int(progress.value / progress.max_value * width) 

88 return marker * length 

89 else: 

90 return marker 

91 

92 if isinstance(marker, str): 

93 marker = converters.to_unicode(marker) 

94 assert ( 

95 utils.len_color(marker) == 1 

96 ), 'Markers are required to be 1 char' 

97 return wrapper(_marker, wrap) 

98 else: 

99 return wrapper(marker, wrap) 

100 

101 

102class FormatWidgetMixin(abc.ABC): 

103 '''Mixin to format widgets using a formatstring 

104 

105 Variables available: 

106 - max_value: The maximum value (can be None with iterators) 

107 - value: The current value 

108 - total_seconds_elapsed: The seconds since the bar started 

109 - seconds_elapsed: The seconds since the bar started modulo 60 

110 - minutes_elapsed: The minutes since the bar started modulo 60 

111 - hours_elapsed: The hours since the bar started modulo 24 

112 - days_elapsed: The hours since the bar started 

113 - time_elapsed: Shortcut for HH:MM:SS time since the bar started including 

114 days 

115 - percentage: Percentage as a float 

116 ''' 

117 

118 def __init__(self, format: str, new_style: bool = False, **kwargs): 

119 self.new_style = new_style 

120 self.format = format 

121 

122 def get_format( 

123 self, 

124 progress: ProgressBarMixinBase, 

125 data: Data, 

126 format: types.Optional[str] = None, 

127 ) -> str: 

128 return format or self.format 

129 

130 def __call__( 

131 self, 

132 progress: ProgressBarMixinBase, 

133 data: Data, 

134 format: types.Optional[str] = None, 

135 ) -> str: 

136 '''Formats the widget into a string''' 

137 format = self.get_format(progress, data, format) 

138 try: 

139 if self.new_style: 

140 return format.format(**data) 

141 else: 

142 return format % data 

143 except (TypeError, KeyError): 

144 print('Error while formatting %r' % format, file=sys.stderr) 

145 pprint.pprint(data, stream=sys.stderr) 

146 raise 

147 

148 

149class WidthWidgetMixin(abc.ABC): 

150 '''Mixing to make sure widgets are only visible if the screen is within a 

151 specified size range so the progressbar fits on both large and small 

152 screens. 

153 

154 Variables available: 

155 - min_width: Only display the widget if at least `min_width` is left 

156 - max_width: Only display the widget if at most `max_width` is left 

157 

158 >>> class Progress: 

159 ... term_width = 0 

160 

161 >>> WidthWidgetMixin(5, 10).check_size(Progress) 

162 False 

163 >>> Progress.term_width = 5 

164 >>> WidthWidgetMixin(5, 10).check_size(Progress) 

165 True 

166 >>> Progress.term_width = 10 

167 >>> WidthWidgetMixin(5, 10).check_size(Progress) 

168 True 

169 >>> Progress.term_width = 11 

170 >>> WidthWidgetMixin(5, 10).check_size(Progress) 

171 False 

172 ''' 

173 

174 def __init__(self, min_width=None, max_width=None, **kwargs): 

175 self.min_width = min_width 

176 self.max_width = max_width 

177 

178 def check_size(self, progress: ProgressBarMixinBase): 

179 if self.min_width and self.min_width > progress.term_width: 

180 return False 

181 elif self.max_width and self.max_width < progress.term_width: 

182 return False 

183 else: 

184 return True 

185 

186 

187class WidgetBase(WidthWidgetMixin, metaclass=abc.ABCMeta): 

188 '''The base class for all widgets 

189 

190 The ProgressBar will call the widget's update value when the widget should 

191 be updated. The widget's size may change between calls, but the widget may 

192 display incorrectly if the size changes drastically and repeatedly. 

193 

194 The INTERVAL timedelta informs the ProgressBar that it should be 

195 updated more often because it is time sensitive. 

196 

197 The widgets are only visible if the screen is within a 

198 specified size range so the progressbar fits on both large and small 

199 screens. 

200 

201 WARNING: Widgets can be shared between multiple progressbars so any state 

202 information specific to a progressbar should be stored within the 

203 progressbar instead of the widget. 

204 

205 Variables available: 

206 - min_width: Only display the widget if at least `min_width` is left 

207 - max_width: Only display the widget if at most `max_width` is left 

208 - weight: Widgets with a higher `weigth` will be calculated before widgets 

209 with a lower one 

210 - copy: Copy this widget when initializing the progress bar so the 

211 progressbar can be reused. Some widgets such as the FormatCustomText 

212 require the shared state so this needs to be optional 

213 

214 ''' 

215 

216 copy = True 

217 

218 @abc.abstractmethod 

219 def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str: 

220 '''Updates the widget. 

221 

222 progress - a reference to the calling ProgressBar 

223 ''' 

224 

225 

226class AutoWidthWidgetBase(WidgetBase, metaclass=abc.ABCMeta): 

227 '''The base class for all variable width widgets. 

228 

229 This widget is much like the \\hfill command in TeX, it will expand to 

230 fill the line. You can use more than one in the same line, and they will 

231 all have the same width, and together will fill the line. 

232 ''' 

233 

234 @abc.abstractmethod 

235 def __call__( 

236 self, 

237 progress: ProgressBarMixinBase, 

238 data: Data, 

239 width: int = 0, 

240 ) -> str: 

241 '''Updates the widget providing the total width the widget must fill. 

242 

243 progress - a reference to the calling ProgressBar 

244 width - The total width the widget must fill 

245 ''' 

246 

247 

248class TimeSensitiveWidgetBase(WidgetBase, metaclass=abc.ABCMeta): 

249 '''The base class for all time sensitive widgets. 

250 

251 Some widgets like timers would become out of date unless updated at least 

252 every `INTERVAL` 

253 ''' 

254 

255 INTERVAL = datetime.timedelta(milliseconds=100) 

256 

257 

258class FormatLabel(FormatWidgetMixin, WidgetBase): 

259 '''Displays a formatted label 

260 

261 >>> label = FormatLabel('%(value)s', min_width=5, max_width=10) 

262 >>> class Progress: 

263 ... pass 

264 >>> label = FormatLabel('{value} :: {value:^6}', new_style=True) 

265 >>> str(label(Progress, dict(value='test'))) 

266 'test :: test ' 

267 

268 ''' 

269 

270 mapping = { 

271 'finished': ('end_time', None), 

272 'last_update': ('last_update_time', None), 

273 'max': ('max_value', None), 

274 'seconds': ('seconds_elapsed', None), 

275 'start': ('start_time', None), 

276 'elapsed': ('total_seconds_elapsed', utils.format_time), 

277 'value': ('value', None), 

278 } 

279 

280 def __init__(self, format: str, **kwargs): 

281 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

282 WidgetBase.__init__(self, **kwargs) 

283 

284 def __call__( 

285 self, 

286 progress: ProgressBarMixinBase, 

287 data: Data, 

288 format: types.Optional[str] = None, 

289 ): 

290 for name, (key, transform) in self.mapping.items(): 

291 try: 

292 if transform is None: 

293 data[name] = data[key] 

294 else: 

295 data[name] = transform(data[key]) 

296 except (KeyError, ValueError, IndexError): # pragma: no cover 

297 pass 

298 

299 return FormatWidgetMixin.__call__(self, progress, data, format) 

300 

301 

302class Timer(FormatLabel, TimeSensitiveWidgetBase): 

303 '''WidgetBase which displays the elapsed seconds.''' 

304 

305 def __init__(self, format='Elapsed Time: %(elapsed)s', **kwargs): 

306 if '%s' in format and '%(elapsed)s' not in format: 

307 format = format.replace('%s', '%(elapsed)s') 

308 

309 FormatLabel.__init__(self, format=format, **kwargs) 

310 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

311 

312 # This is exposed as a static method for backwards compatibility 

313 format_time = staticmethod(utils.format_time) 

314 

315 

316class SamplesMixin(TimeSensitiveWidgetBase, metaclass=abc.ABCMeta): 

317 ''' 

318 Mixing for widgets that average multiple measurements 

319 

320 Note that samples can be either an integer or a timedelta to indicate a 

321 certain amount of time 

322 

323 >>> class progress: 

324 ... last_update_time = datetime.datetime.now() 

325 ... value = 1 

326 ... extra = dict() 

327 

328 >>> samples = SamplesMixin(samples=2) 

329 >>> samples(progress, None, True) 

330 (None, None) 

331 >>> progress.last_update_time += datetime.timedelta(seconds=1) 

332 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0) 

333 True 

334 

335 >>> progress.last_update_time += datetime.timedelta(seconds=1) 

336 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0) 

337 True 

338 

339 >>> samples = SamplesMixin(samples=datetime.timedelta(seconds=1)) 

340 >>> _, value = samples(progress, None) 

341 >>> value 

342 [1, 1] 

343 

344 >>> samples(progress, None, True) == (datetime.timedelta(seconds=1), 0) 

345 True 

346 ''' 

347 

348 def __init__( 

349 self, 

350 samples=datetime.timedelta(seconds=2), 

351 key_prefix=None, 

352 **kwargs, 

353 ): 

354 self.samples = samples 

355 self.key_prefix = ( 

356 key_prefix if key_prefix else self.__class__.__name__ 

357 ) + '_' 

358 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

359 

360 def get_sample_times(self, progress: ProgressBarMixinBase, data: Data): 

361 return progress.extra.setdefault(self.key_prefix + 'sample_times', []) 

362 

363 def get_sample_values(self, progress: ProgressBarMixinBase, data: Data): 

364 return progress.extra.setdefault(self.key_prefix + 'sample_values', []) 

365 

366 def __call__( 

367 self, progress: ProgressBarMixinBase, data: Data, delta: bool = False 

368 ): 

369 sample_times = self.get_sample_times(progress, data) 

370 sample_values = self.get_sample_values(progress, data) 

371 

372 if sample_times: 

373 sample_time = sample_times[-1] 

374 else: 

375 sample_time = datetime.datetime.min 

376 

377 if progress.last_update_time - sample_time > self.INTERVAL: 

378 # Add a sample but limit the size to `num_samples` 

379 sample_times.append(progress.last_update_time) 

380 sample_values.append(progress.value) 

381 

382 if isinstance(self.samples, datetime.timedelta): 

383 minimum_time = progress.last_update_time - self.samples 

384 minimum_value = sample_values[-1] 

385 while ( 

386 sample_times[2:] 

387 and minimum_time > sample_times[1] 

388 and minimum_value > sample_values[1] 

389 ): 

390 sample_times.pop(0) 

391 sample_values.pop(0) 

392 else: 

393 if len(sample_times) > self.samples: 

394 sample_times.pop(0) 

395 sample_values.pop(0) 

396 

397 if delta: 

398 delta_time = sample_times[-1] - sample_times[0] 

399 delta_value = sample_values[-1] - sample_values[0] 

400 if delta_time: 

401 return delta_time, delta_value 

402 else: 

403 return None, None 

404 else: 

405 return sample_times, sample_values 

406 

407 

408class ETA(Timer): 

409 '''WidgetBase which attempts to estimate the time of arrival.''' 

410 

411 def __init__( 

412 self, 

413 format_not_started='ETA: --:--:--', 

414 format_finished='Time: %(elapsed)8s', 

415 format='ETA: %(eta)8s', 

416 format_zero='ETA: 00:00:00', 

417 format_NA='ETA: N/A', 

418 **kwargs, 

419 ): 

420 

421 if '%s' in format and '%(eta)s' not in format: 

422 format = format.replace('%s', '%(eta)s') 

423 

424 Timer.__init__(self, **kwargs) 

425 self.format_not_started = format_not_started 

426 self.format_finished = format_finished 

427 self.format = format 

428 self.format_zero = format_zero 

429 self.format_NA = format_NA 

430 

431 def _calculate_eta( 

432 self, progress: ProgressBarMixinBase, data: Data, value, elapsed 

433 ): 

434 '''Updates the widget to show the ETA or total time when finished.''' 

435 if elapsed: 

436 # The max() prevents zero division errors 

437 per_item = elapsed.total_seconds() / max(value, 1e-6) 

438 remaining = progress.max_value - data['value'] 

439 eta_seconds = remaining * per_item 

440 else: 

441 eta_seconds = 0 

442 

443 return eta_seconds 

444 

445 def __call__( 

446 self, 

447 progress: ProgressBarMixinBase, 

448 data: Data, 

449 value=None, 

450 elapsed=None, 

451 ): 

452 '''Updates the widget to show the ETA or total time when finished.''' 

453 if value is None: 

454 value = data['value'] 

455 

456 if elapsed is None: 

457 elapsed = data['time_elapsed'] 

458 

459 ETA_NA = False 

460 try: 

461 data['eta_seconds'] = self._calculate_eta( 

462 progress, data, value=value, elapsed=elapsed 

463 ) 

464 except TypeError: 

465 data['eta_seconds'] = None 

466 ETA_NA = True 

467 

468 data['eta'] = None 

469 if data['eta_seconds']: 

470 try: 

471 data['eta'] = utils.format_time(data['eta_seconds']) 

472 except (ValueError, OverflowError): # pragma: no cover 

473 pass 

474 

475 if data['value'] == progress.min_value: 

476 format = self.format_not_started 

477 elif progress.end_time: 

478 format = self.format_finished 

479 elif data['eta']: 

480 format = self.format 

481 elif ETA_NA: 

482 format = self.format_NA 

483 else: 

484 format = self.format_zero 

485 

486 return Timer.__call__(self, progress, data, format=format) 

487 

488 

489class AbsoluteETA(ETA): 

490 '''Widget which attempts to estimate the absolute time of arrival.''' 

491 

492 def _calculate_eta( 

493 self, progress: ProgressBarMixinBase, data: Data, value, elapsed 

494 ): 

495 eta_seconds = ETA._calculate_eta(self, progress, data, value, elapsed) 

496 now = datetime.datetime.now() 

497 try: 

498 return now + datetime.timedelta(seconds=eta_seconds) 

499 except OverflowError: # pragma: no cover 

500 return datetime.datetime.max 

501 

502 def __init__( 

503 self, 

504 format_not_started='Estimated finish time: ----/--/-- --:--:--', 

505 format_finished='Finished at: %(elapsed)s', 

506 format='Estimated finish time: %(eta)s', 

507 **kwargs, 

508 ): 

509 ETA.__init__( 

510 self, 

511 format_not_started=format_not_started, 

512 format_finished=format_finished, 

513 format=format, 

514 **kwargs, 

515 ) 

516 

517 

518class AdaptiveETA(ETA, SamplesMixin): 

519 '''WidgetBase which attempts to estimate the time of arrival. 

520 

521 Uses a sampled average of the speed based on the 10 last updates. 

522 Very convenient for resuming the progress halfway. 

523 ''' 

524 

525 def __init__(self, **kwargs): 

526 ETA.__init__(self, **kwargs) 

527 SamplesMixin.__init__(self, **kwargs) 

528 

529 def __call__( 

530 self, 

531 progress: ProgressBarMixinBase, 

532 data: Data, 

533 value=None, 

534 elapsed=None, 

535 ): 

536 elapsed, value = SamplesMixin.__call__( 

537 self, progress, data, delta=True 

538 ) 

539 if not elapsed: 

540 value = None 

541 elapsed = 0 

542 

543 return ETA.__call__(self, progress, data, value=value, elapsed=elapsed) 

544 

545 

546class DataSize(FormatWidgetMixin, WidgetBase): 

547 ''' 

548 Widget for showing an amount of data transferred/processed. 

549 

550 Automatically formats the value (assumed to be a count of bytes) with an 

551 appropriate sized unit, based on the IEC binary prefixes (powers of 1024). 

552 ''' 

553 

554 def __init__( 

555 self, 

556 variable='value', 

557 format='%(scaled)5.1f %(prefix)s%(unit)s', 

558 unit='B', 

559 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), 

560 **kwargs, 

561 ): 

562 self.variable = variable 

563 self.unit = unit 

564 self.prefixes = prefixes 

565 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

566 WidgetBase.__init__(self, **kwargs) 

567 

568 def __call__( 

569 self, 

570 progress: ProgressBarMixinBase, 

571 data: Data, 

572 format: types.Optional[str] = None, 

573 ): 

574 value = data[self.variable] 

575 if value is not None: 

576 scaled, power = utils.scale_1024(value, len(self.prefixes)) 

577 else: 

578 scaled = power = 0 

579 

580 data['scaled'] = scaled 

581 data['prefix'] = self.prefixes[power] 

582 data['unit'] = self.unit 

583 

584 return FormatWidgetMixin.__call__(self, progress, data, format) 

585 

586 

587class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): 

588 ''' 

589 Widget for showing the current transfer speed (useful for file transfers). 

590 ''' 

591 

592 def __init__( 

593 self, 

594 format='%(scaled)5.1f %(prefix)s%(unit)-s/s', 

595 inverse_format='%(scaled)5.1f s/%(prefix)s%(unit)-s', 

596 unit='B', 

597 prefixes=('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'), 

598 **kwargs, 

599 ): 

600 self.unit = unit 

601 self.prefixes = prefixes 

602 self.inverse_format = inverse_format 

603 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

604 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

605 

606 def _speed(self, value, elapsed): 

607 speed = float(value) / elapsed 

608 return utils.scale_1024(speed, len(self.prefixes)) 

609 

610 def __call__( 

611 self, 

612 progress: ProgressBarMixinBase, 

613 data, 

614 value=None, 

615 total_seconds_elapsed=None, 

616 ): 

617 '''Updates the widget with the current SI prefixed speed.''' 

618 if value is None: 

619 value = data['value'] 

620 

621 elapsed = utils.deltas_to_seconds( 

622 total_seconds_elapsed, data['total_seconds_elapsed'] 

623 ) 

624 

625 if ( 

626 value is not None 

627 and elapsed is not None 

628 and elapsed > 2e-6 

629 and value > 2e-6 

630 ): # =~ 0 

631 scaled, power = self._speed(value, elapsed) 

632 else: 

633 scaled = power = 0 

634 

635 data['unit'] = self.unit 

636 if power == 0 and scaled < 0.1: 

637 if scaled > 0: 

638 scaled = 1 / scaled 

639 data['scaled'] = scaled 

640 data['prefix'] = self.prefixes[0] 

641 return FormatWidgetMixin.__call__( 

642 self, progress, data, self.inverse_format 

643 ) 

644 else: 

645 data['scaled'] = scaled 

646 data['prefix'] = self.prefixes[power] 

647 return FormatWidgetMixin.__call__(self, progress, data) 

648 

649 

650class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): 

651 '''Widget for showing the transfer speed based on the last X samples''' 

652 

653 def __init__(self, **kwargs): 

654 FileTransferSpeed.__init__(self, **kwargs) 

655 SamplesMixin.__init__(self, **kwargs) 

656 

657 def __call__( 

658 self, 

659 progress: ProgressBarMixinBase, 

660 data, 

661 value=None, 

662 total_seconds_elapsed=None, 

663 ): 

664 elapsed, value = SamplesMixin.__call__( 

665 self, progress, data, delta=True 

666 ) 

667 return FileTransferSpeed.__call__(self, progress, data, value, elapsed) 

668 

669 

670class AnimatedMarker(TimeSensitiveWidgetBase): 

671 '''An animated marker for the progress bar which defaults to appear as if 

672 it were rotating. 

673 ''' 

674 

675 def __init__( 

676 self, 

677 markers='|/-\\', 

678 default=None, 

679 fill='', 

680 marker_wrap=None, 

681 fill_wrap=None, 

682 **kwargs, 

683 ): 

684 self.markers = markers 

685 self.marker_wrap = create_wrapper(marker_wrap) 

686 self.default = default or markers[0] 

687 self.fill_wrap = create_wrapper(fill_wrap) 

688 self.fill = create_marker(fill, self.fill_wrap) if fill else None 

689 WidgetBase.__init__(self, **kwargs) 

690 

691 def __call__(self, progress: ProgressBarMixinBase, data: Data, width=None): 

692 '''Updates the widget to show the next marker or the first marker when 

693 finished''' 

694 

695 if progress.end_time: 

696 return self.default 

697 

698 marker = self.markers[data['updates'] % len(self.markers)] 

699 if self.marker_wrap: 

700 marker = self.marker_wrap.format(marker) 

701 

702 if self.fill: 

703 # Cut the last character so we can replace it with our marker 

704 fill = self.fill( 

705 progress, 

706 data, 

707 width - progress.custom_len(marker), # type: ignore 

708 ) 

709 else: 

710 fill = '' 

711 

712 # Python 3 returns an int when indexing bytes 

713 if isinstance(marker, int): # pragma: no cover 

714 marker = bytes(marker) 

715 fill = fill.encode() 

716 else: 

717 # cast fill to the same type as marker 

718 fill = type(marker)(fill) 

719 

720 return fill + marker # type: ignore 

721 

722 

723# Alias for backwards compatibility 

724RotatingMarker = AnimatedMarker 

725 

726 

727class Counter(FormatWidgetMixin, WidgetBase): 

728 '''Displays the current count''' 

729 

730 def __init__(self, format='%(value)d', **kwargs): 

731 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

732 WidgetBase.__init__(self, format=format, **kwargs) 

733 

734 def __call__( 

735 self, progress: ProgressBarMixinBase, data: Data, format=None 

736 ): 

737 return FormatWidgetMixin.__call__(self, progress, data, format) 

738 

739 

740class Percentage(FormatWidgetMixin, WidgetBase): 

741 '''Displays the current percentage as a number with a percent sign.''' 

742 

743 def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): 

744 self.na = na 

745 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

746 WidgetBase.__init__(self, format=format, **kwargs) 

747 

748 def get_format( 

749 self, progress: ProgressBarMixinBase, data: Data, format=None 

750 ): 

751 # If percentage is not available, display N/A% 

752 percentage = data.get('percentage', base.Undefined) 

753 if not percentage and percentage != 0: 

754 return self.na 

755 

756 return FormatWidgetMixin.get_format(self, progress, data, format) 

757 

758 

759class SimpleProgress(FormatWidgetMixin, WidgetBase): 

760 '''Returns progress as a count of the total (e.g.: "5 of 47")''' 

761 

762 max_width_cache: dict[ 

763 types.Union[str, tuple[float, float | types.Type[base.UnknownLength]]], 

764 types.Optional[int], 

765 ] 

766 

767 DEFAULT_FORMAT = '%(value_s)s of %(max_value_s)s' 

768 

769 def __init__(self, format=DEFAULT_FORMAT, **kwargs): 

770 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

771 WidgetBase.__init__(self, format=format, **kwargs) 

772 self.max_width_cache = dict() 

773 self.max_width_cache['default'] = self.max_width or 0 

774 

775 def __call__( 

776 self, progress: ProgressBarMixinBase, data: Data, format=None 

777 ): 

778 # If max_value is not available, display N/A 

779 if data.get('max_value'): 

780 data['max_value_s'] = data.get('max_value') 

781 else: 

782 data['max_value_s'] = 'N/A' 

783 

784 # if value is not available it's the zeroth iteration 

785 if data.get('value'): 

786 data['value_s'] = data['value'] 

787 else: 

788 data['value_s'] = 0 

789 

790 formatted = FormatWidgetMixin.__call__( 

791 self, progress, data, format=format 

792 ) 

793 

794 # Guess the maximum width from the min and max value 

795 key = progress.min_value, progress.max_value 

796 max_width: types.Optional[int] = self.max_width_cache.get( 

797 key, self.max_width 

798 ) 

799 if not max_width: 

800 temporary_data = data.copy() 

801 for value in key: 

802 if value is None: # pragma: no cover 

803 continue 

804 

805 temporary_data['value'] = value 

806 width = progress.custom_len( 

807 FormatWidgetMixin.__call__( 

808 self, progress, temporary_data, format=format 

809 ) 

810 ) 

811 if width: # pragma: no branch 

812 max_width = max(max_width or 0, width) 

813 

814 self.max_width_cache[key] = max_width 

815 

816 # Adjust the output to have a consistent size in all cases 

817 if max_width: # pragma: no branch 

818 formatted = formatted.rjust(max_width) 

819 

820 return formatted 

821 

822 

823class Bar(AutoWidthWidgetBase): 

824 '''A progress bar which stretches to fill the line.''' 

825 

826 def __init__( 

827 self, 

828 marker='#', 

829 left='|', 

830 right='|', 

831 fill=' ', 

832 fill_left=True, 

833 marker_wrap=None, 

834 **kwargs, 

835 ): 

836 '''Creates a customizable progress bar. 

837 

838 The callable takes the same parameters as the `__call__` method 

839 

840 marker - string or callable object to use as a marker 

841 left - string or callable object to use as a left border 

842 right - string or callable object to use as a right border 

843 fill - character to use for the empty part of the progress bar 

844 fill_left - whether to fill from the left or the right 

845 ''' 

846 

847 self.marker = create_marker(marker, marker_wrap) 

848 self.left = string_or_lambda(left) 

849 self.right = string_or_lambda(right) 

850 self.fill = string_or_lambda(fill) 

851 self.fill_left = fill_left 

852 

853 AutoWidthWidgetBase.__init__(self, **kwargs) 

854 

855 def __call__( 

856 self, 

857 progress: ProgressBarMixinBase, 

858 data: Data, 

859 width: int = 0, 

860 ): 

861 '''Updates the progress bar and its subcomponents''' 

862 

863 left = converters.to_unicode(self.left(progress, data, width)) 

864 right = converters.to_unicode(self.right(progress, data, width)) 

865 width -= progress.custom_len(left) + progress.custom_len(right) 

866 marker = converters.to_unicode(self.marker(progress, data, width)) 

867 fill = converters.to_unicode(self.fill(progress, data, width)) 

868 

869 # Make sure we ignore invisible characters when filling 

870 width += len(marker) - progress.custom_len(marker) 

871 

872 if self.fill_left: 

873 marker = marker.ljust(width, fill) 

874 else: 

875 marker = marker.rjust(width, fill) 

876 

877 return left + marker + right 

878 

879 

880class ReverseBar(Bar): 

881 '''A bar which has a marker that goes from right to left''' 

882 

883 def __init__( 

884 self, 

885 marker='#', 

886 left='|', 

887 right='|', 

888 fill=' ', 

889 fill_left=False, 

890 **kwargs, 

891 ): 

892 '''Creates a customizable progress bar. 

893 

894 marker - string or updatable object to use as a marker 

895 left - string or updatable object to use as a left border 

896 right - string or updatable object to use as a right border 

897 fill - character to use for the empty part of the progress bar 

898 fill_left - whether to fill from the left or the right 

899 ''' 

900 Bar.__init__( 

901 self, 

902 marker=marker, 

903 left=left, 

904 right=right, 

905 fill=fill, 

906 fill_left=fill_left, 

907 **kwargs, 

908 ) 

909 

910 

911class BouncingBar(Bar, TimeSensitiveWidgetBase): 

912 '''A bar which has a marker which bounces from side to side.''' 

913 

914 INTERVAL = datetime.timedelta(milliseconds=100) 

915 

916 def __call__( 

917 self, 

918 progress: ProgressBarMixinBase, 

919 data: Data, 

920 width: int = 0, 

921 ): 

922 '''Updates the progress bar and its subcomponents''' 

923 

924 left = converters.to_unicode(self.left(progress, data, width)) 

925 right = converters.to_unicode(self.right(progress, data, width)) 

926 width -= progress.custom_len(left) + progress.custom_len(right) 

927 marker = converters.to_unicode(self.marker(progress, data, width)) 

928 

929 fill = converters.to_unicode(self.fill(progress, data, width)) 

930 

931 if width: # pragma: no branch 

932 value = int( 

933 data['total_seconds_elapsed'] / self.INTERVAL.total_seconds() 

934 ) 

935 

936 a = value % width 

937 b = width - a - 1 

938 if value % (width * 2) >= width: 

939 a, b = b, a 

940 

941 if self.fill_left: 

942 marker = a * fill + marker + b * fill 

943 else: 

944 marker = b * fill + marker + a * fill 

945 

946 return left + marker + right 

947 

948 

949class FormatCustomText(FormatWidgetMixin, WidgetBase): 

950 mapping: types.Dict[str, types.Any] = {} 

951 copy = False 

952 

953 def __init__( 

954 self, 

955 format: str, 

956 mapping: types.Optional[types.Dict[str, types.Any]] = None, 

957 **kwargs, 

958 ): 

959 self.format = format 

960 self.mapping = mapping or self.mapping 

961 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

962 WidgetBase.__init__(self, **kwargs) 

963 

964 def update_mapping(self, **mapping: types.Dict[str, types.Any]): 

965 self.mapping.update(mapping) 

966 

967 def __call__( 

968 self, 

969 progress: ProgressBarMixinBase, 

970 data: Data, 

971 format: types.Optional[str] = None, 

972 ): 

973 return FormatWidgetMixin.__call__( 

974 self, progress, self.mapping, format or self.format 

975 ) 

976 

977 

978class VariableMixin: 

979 '''Mixin to display a custom user variable''' 

980 

981 def __init__(self, name, **kwargs): 

982 if not isinstance(name, str): 

983 raise TypeError('Variable(): argument must be a string') 

984 if len(name.split()) > 1: 

985 raise ValueError('Variable(): argument must be single word') 

986 self.name = name 

987 

988 

989class MultiRangeBar(Bar, VariableMixin): 

990 ''' 

991 A bar with multiple sub-ranges, each represented by a different symbol 

992 

993 The various ranges are represented on a user-defined variable, formatted as 

994 

995 .. code-block:: python 

996 

997 [ 

998 ['Symbol1', amount1], 

999 ['Symbol2', amount2], 

1000 ... 

1001 ] 

1002 ''' 

1003 

1004 def __init__(self, name, markers, **kwargs): 

1005 VariableMixin.__init__(self, name) 

1006 Bar.__init__(self, **kwargs) 

1007 self.markers = [string_or_lambda(marker) for marker in markers] 

1008 

1009 def get_values(self, progress: ProgressBarMixinBase, data: Data): 

1010 return data['variables'][self.name] or [] 

1011 

1012 def __call__( 

1013 self, 

1014 progress: ProgressBarMixinBase, 

1015 data: Data, 

1016 width: int = 0, 

1017 ): 

1018 '''Updates the progress bar and its subcomponents''' 

1019 

1020 left = converters.to_unicode(self.left(progress, data, width)) 

1021 right = converters.to_unicode(self.right(progress, data, width)) 

1022 width -= progress.custom_len(left) + progress.custom_len(right) 

1023 values = self.get_values(progress, data) 

1024 

1025 values_sum = sum(values) 

1026 if width and values_sum: 

1027 middle = '' 

1028 values_accumulated = 0 

1029 width_accumulated = 0 

1030 for marker, value in zip(self.markers, values): 

1031 marker = converters.to_unicode(marker(progress, data, width)) 

1032 assert progress.custom_len(marker) == 1 

1033 

1034 values_accumulated += value 

1035 item_width = int(values_accumulated / values_sum * width) 

1036 item_width -= width_accumulated 

1037 width_accumulated += item_width 

1038 middle += item_width * marker 

1039 else: 

1040 fill = converters.to_unicode(self.fill(progress, data, width)) 

1041 assert progress.custom_len(fill) == 1 

1042 middle = fill * width 

1043 

1044 return left + middle + right 

1045 

1046 

1047class MultiProgressBar(MultiRangeBar): 

1048 def __init__( 

1049 self, 

1050 name, 

1051 # NOTE: the markers are not whitespace even though some 

1052 # terminals don't show the characters correctly! 

1053 markers=' ▁▂▃▄▅▆▇█', 

1054 **kwargs, 

1055 ): 

1056 MultiRangeBar.__init__( 

1057 self, 

1058 name=name, 

1059 markers=list(reversed(markers)), 

1060 **kwargs, 

1061 ) 

1062 

1063 def get_values(self, progress: ProgressBarMixinBase, data: Data): 

1064 ranges = [0.0] * len(self.markers) 

1065 for value in data['variables'][self.name] or []: 

1066 if not isinstance(value, (int, float)): 

1067 # Progress is (value, max) 

1068 progress_value, progress_max = value 

1069 value = float(progress_value) / float(progress_max) 

1070 

1071 if not 0 <= value <= 1: 

1072 raise ValueError( 

1073 'Range value needs to be in the range [0..1], got %s' 

1074 % value 

1075 ) 

1076 

1077 range_ = value * (len(ranges) - 1) 

1078 pos = int(range_) 

1079 frac = range_ % 1 

1080 ranges[pos] += 1 - frac 

1081 if frac: 

1082 ranges[pos + 1] += frac 

1083 

1084 if self.fill_left: 

1085 ranges = list(reversed(ranges)) 

1086 

1087 return ranges 

1088 

1089 

1090class GranularMarkers: 

1091 smooth = ' ▏▎▍▌▋▊▉█' 

1092 bar = ' ▁▂▃▄▅▆▇█' 

1093 snake = ' ▖▌▛█' 

1094 fade_in = ' ░▒▓█' 

1095 dots = ' ⡀⡄⡆⡇⣇⣧⣷⣿' 

1096 growing_circles = ' .oO' 

1097 

1098 

1099class GranularBar(AutoWidthWidgetBase): 

1100 '''A progressbar that can display progress at a sub-character granularity 

1101 by using multiple marker characters. 

1102 

1103 Examples of markers: 

1104 - Smooth: ` ▏▎▍▌▋▊▉█` (default) 

1105 - Bar: ` ▁▂▃▄▅▆▇█` 

1106 - Snake: ` ▖▌▛█` 

1107 - Fade in: ` ░▒▓█` 

1108 - Dots: ` ⡀⡄⡆⡇⣇⣧⣷⣿` 

1109 - Growing circles: ` .oO` 

1110 

1111 The markers can be accessed through GranularMarkers. GranularMarkers.dots 

1112 for example 

1113 ''' 

1114 

1115 def __init__( 

1116 self, 

1117 markers=GranularMarkers.smooth, 

1118 left='|', 

1119 right='|', 

1120 **kwargs, 

1121 ): 

1122 '''Creates a customizable progress bar. 

1123 

1124 markers - string of characters to use as granular progress markers. The 

1125 first character should represent 0% and the last 100%. 

1126 Ex: ` .oO`. 

1127 left - string or callable object to use as a left border 

1128 right - string or callable object to use as a right border 

1129 ''' 

1130 self.markers = markers 

1131 self.left = string_or_lambda(left) 

1132 self.right = string_or_lambda(right) 

1133 

1134 AutoWidthWidgetBase.__init__(self, **kwargs) 

1135 

1136 def __call__( 

1137 self, 

1138 progress: ProgressBarMixinBase, 

1139 data: Data, 

1140 width: int = 0, 

1141 ): 

1142 left = converters.to_unicode(self.left(progress, data, width)) 

1143 right = converters.to_unicode(self.right(progress, data, width)) 

1144 width -= progress.custom_len(left) + progress.custom_len(right) 

1145 

1146 max_value = progress.max_value 

1147 # mypy doesn't get that the first part of the if statement makes sure 

1148 # we get the correct type 

1149 if ( 

1150 max_value is not base.UnknownLength 

1151 and max_value > 0 # type: ignore 

1152 ): 

1153 percent = progress.value / max_value # type: ignore 

1154 else: 

1155 percent = 0 

1156 

1157 num_chars = percent * width 

1158 

1159 marker = self.markers[-1] * int(num_chars) 

1160 

1161 marker_idx = int((num_chars % 1) * (len(self.markers) - 1)) 

1162 if marker_idx: 

1163 marker += self.markers[marker_idx] 

1164 

1165 marker = converters.to_unicode(marker) 

1166 

1167 # Make sure we ignore invisible characters when filling 

1168 width += len(marker) - progress.custom_len(marker) 

1169 marker = marker.ljust(width, self.markers[0]) 

1170 

1171 return left + marker + right 

1172 

1173 

1174class FormatLabelBar(FormatLabel, Bar): 

1175 '''A bar which has a formatted label in the center.''' 

1176 

1177 def __init__(self, format, **kwargs): 

1178 FormatLabel.__init__(self, format, **kwargs) 

1179 Bar.__init__(self, **kwargs) 

1180 

1181 def __call__( # type: ignore 

1182 self, 

1183 progress: ProgressBarMixinBase, 

1184 data: Data, 

1185 width: int = 0, 

1186 format: FormatString = None, 

1187 ): 

1188 center = FormatLabel.__call__(self, progress, data, format=format) 

1189 bar = Bar.__call__(self, progress, data, width) 

1190 

1191 # Aligns the center of the label to the center of the bar 

1192 center_len = progress.custom_len(center) 

1193 center_left = int((width - center_len) / 2) 

1194 center_right = center_left + center_len 

1195 return bar[:center_left] + center + bar[center_right:] 

1196 

1197 

1198class PercentageLabelBar(Percentage, FormatLabelBar): 

1199 '''A bar which displays the current percentage in the center.''' 

1200 

1201 # %3d adds an extra space that makes it look off-center 

1202 # %2d keeps the label somewhat consistently in-place 

1203 def __init__(self, format='%(percentage)2d%%', na='N/A%%', **kwargs): 

1204 Percentage.__init__(self, format, na=na, **kwargs) 

1205 FormatLabelBar.__init__(self, format, **kwargs) 

1206 

1207 def __call__( # type: ignore 

1208 self, 

1209 progress: ProgressBarMixinBase, 

1210 data: Data, 

1211 width: int = 0, 

1212 format: FormatString = None, 

1213 ): 

1214 return super().__call__(progress, data, width, format=format) 

1215 

1216 

1217class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 

1218 '''Displays a custom variable.''' 

1219 

1220 def __init__( 

1221 self, 

1222 name, 

1223 format='{name}: {formatted_value}', 

1224 width=6, 

1225 precision=3, 

1226 **kwargs, 

1227 ): 

1228 '''Creates a Variable associated with the given name.''' 

1229 self.format = format 

1230 self.width = width 

1231 self.precision = precision 

1232 VariableMixin.__init__(self, name=name) 

1233 WidgetBase.__init__(self, **kwargs) 

1234 

1235 def __call__( 

1236 self, 

1237 progress: ProgressBarMixinBase, 

1238 data: Data, 

1239 format: types.Optional[str] = None, 

1240 ): 

1241 value = data['variables'][self.name] 

1242 context = data.copy() 

1243 context['value'] = value 

1244 context['name'] = self.name 

1245 context['width'] = self.width 

1246 context['precision'] = self.precision 

1247 

1248 try: 

1249 # Make sure to try and cast the value first, otherwise the 

1250 # formatting will generate warnings/errors on newer Python releases 

1251 value = float(value) 

1252 fmt = '{value:{width}.{precision}}' 

1253 context['formatted_value'] = fmt.format(**context) 

1254 except (TypeError, ValueError): 

1255 if value: 

1256 context['formatted_value'] = '{value:{width}}'.format( 

1257 **context 

1258 ) 

1259 else: 

1260 context['formatted_value'] = '-' * self.width 

1261 

1262 return self.format.format(**context) 

1263 

1264 

1265class DynamicMessage(Variable): 

1266 '''Kept for backwards compatibility, please use `Variable` instead.''' 

1267 

1268 pass 

1269 

1270 

1271class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 

1272 '''Widget which displays the current (date)time with seconds resolution.''' 

1273 

1274 INTERVAL = datetime.timedelta(seconds=1) 

1275 

1276 def __init__( 

1277 self, 

1278 format='Current Time: %(current_time)s', 

1279 microseconds=False, 

1280 **kwargs, 

1281 ): 

1282 self.microseconds = microseconds 

1283 FormatWidgetMixin.__init__(self, format=format, **kwargs) 

1284 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

1285 

1286 def __call__( 

1287 self, 

1288 progress: ProgressBarMixinBase, 

1289 data: Data, 

1290 format: types.Optional[str] = None, 

1291 ): 

1292 data['current_time'] = self.current_time() 

1293 data['current_datetime'] = self.current_datetime() 

1294 

1295 return FormatWidgetMixin.__call__(self, progress, data, format=format) 

1296 

1297 def current_datetime(self): 

1298 now = datetime.datetime.now() 

1299 if not self.microseconds: 

1300 now = now.replace(microsecond=0) 

1301 

1302 return now 

1303 

1304 def current_time(self): 

1305 return self.current_datetime().time()