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

506 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-26 14:15 +0200

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

2import abc 

3import datetime 

4import functools 

5import pprint 

6import sys 

7 

8from python_utils import converters 

9from python_utils import types 

10 

11from . import base 

12from . import utils 

13 

14if types.TYPE_CHECKING: 

15 from .bar import ProgressBar 

16 

17MAX_DATE = datetime.date.max 

18MAX_TIME = datetime.time.max 

19MAX_DATETIME = datetime.datetime.max 

20 

21 

22def string_or_lambda(input_): 

23 if isinstance(input_, str): 

24 def render_input(progress, data, width): 

25 return input_ % data 

26 

27 return render_input 

28 else: 

29 return input_ 

30 

31 

32def create_wrapper(wrapper): 

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

34 

35 >>> create_wrapper('') 

36 

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

38 a{}b 

39 

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

41 a{}b 

42 ''' 

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

44 a, b = wrapper 

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

46 elif not wrapper: 

47 return 

48 

49 if isinstance(wrapper, str): 

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

51 else: 

52 raise RuntimeError('Pass either a begin/end string as a tuple or a' 

53 ' template string with {}') 

54 

55 return wrapper 

56 

57 

58def wrapper(function, wrapper): 

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

60 begin/end strings 

61 

62 ''' 

63 wrapper = create_wrapper(wrapper) 

64 if not wrapper: 

65 return function 

66 

67 @functools.wraps(function) 

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

69 return wrapper.format(function(*args, **kwargs)) 

70 

71 return wrap 

72 

73 

74def create_marker(marker, wrap=None): 

75 def _marker(progress, data, width): 

76 if progress.max_value is not base.UnknownLength \ 

77 and progress.max_value > 0: 

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

79 return (marker * length) 

80 else: 

81 return marker 

82 

83 if isinstance(marker, str): 

84 marker = converters.to_unicode(marker) 

85 assert utils.len_color(marker) == 1, \ 

86 'Markers are required to be 1 char' 

87 return wrapper(_marker, wrap) 

88 else: 

89 return wrapper(marker, wrap) 

90 

91 

92class FormatWidgetMixin(object): 

93 '''Mixin to format widgets using a formatstring 

94 

95 Variables available: 

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

97 - value: The current value 

98 - total_seconds_elapsed: The seconds since the bar started 

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

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

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

102 - days_elapsed: The hours since the bar started 

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

104 days 

105 - percentage: Percentage as a float 

106 ''' 

107 required_values = [] 

108 

109 def __init__(self, format, new_style=False, **kwargs): 

110 self.new_style = new_style 

111 self.format = format 

112 

113 def get_format(self, progress, data, format=None): 

114 return format or self.format 

115 

116 def __call__(self, progress, data, format=None): 

117 '''Formats the widget into a string''' 

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

119 try: 

120 if self.new_style: 

121 return format.format(**data) 

122 else: 

123 return format % data 

124 except (TypeError, KeyError): 

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

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

127 raise 

128 

129 

130class WidthWidgetMixin(object): 

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

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

133 screens.. 

134 

135 Variables available: 

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

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

138 

139 >>> class Progress(object): 

140 ... term_width = 0 

141 

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

143 False 

144 >>> Progress.term_width = 5 

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

146 True 

147 >>> Progress.term_width = 10 

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

149 True 

150 >>> Progress.term_width = 11 

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

152 False 

153 ''' 

154 

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

156 self.min_width = min_width 

157 self.max_width = max_width 

158 

159 def check_size(self, progress: 'ProgressBar'): 

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

161 return False 

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

163 return False 

164 else: 

165 return True 

166 

167 

168class WidgetBase(WidthWidgetMixin): 

169 __metaclass__ = abc.ABCMeta 

170 '''The base class for all widgets 

171 

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

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

174 display incorrectly if the size changes drastically and repeatedly. 

175 

176 The boolean INTERVAL informs the ProgressBar that it should be 

177 updated more often because it is time sensitive. 

178 

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

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

181 screens. 

182 

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

184 information specific to a progressbar should be stored within the 

185 progressbar instead of the widget. 

186 

187 Variables available: 

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

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

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

191 with a lower one 

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

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

194 require the shared state so this needs to be optional 

195 ''' 

196 copy = True 

197 

198 @abc.abstractmethod 

199 def __call__(self, progress, data): 

200 '''Updates the widget. 

201 

202 progress - a reference to the calling ProgressBar 

203 ''' 

204 

205 

206class AutoWidthWidgetBase(WidgetBase): 

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

208 

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

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

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

212 ''' 

213 

214 @abc.abstractmethod 

215 def __call__(self, progress, data, width): 

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

217 

218 progress - a reference to the calling ProgressBar 

219 width - The total width the widget must fill 

220 ''' 

221 

222 

223class TimeSensitiveWidgetBase(WidgetBase): 

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

225 

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

227 every `INTERVAL` 

228 ''' 

229 INTERVAL = datetime.timedelta(milliseconds=100) 

230 

231 

232class FormatLabel(FormatWidgetMixin, WidgetBase): 

233 '''Displays a formatted label 

234 

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

236 >>> class Progress(object): 

237 ... pass 

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

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

240 'test :: test ' 

241 

242 ''' 

243 

244 mapping = { 

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

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

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

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

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

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

251 'value': ('value', None), 

252 } 

253 

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

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

256 WidgetBase.__init__(self, **kwargs) 

257 

258 def __call__(self, progress, data, **kwargs): 

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

260 try: 

261 if transform is None: 

262 data[name] = data[key] 

263 else: 

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

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

266 pass 

267 

268 return FormatWidgetMixin.__call__(self, progress, data, **kwargs) 

269 

270 

271class Timer(FormatLabel, TimeSensitiveWidgetBase): 

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

273 

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

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

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

277 

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

279 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

280 

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

282 format_time = staticmethod(utils.format_time) 

283 

284 

285class SamplesMixin(TimeSensitiveWidgetBase): 

286 ''' 

287 Mixing for widgets that average multiple measurements 

288 

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

290 certain amount of time 

291 

292 >>> class progress: 

293 ... last_update_time = datetime.datetime.now() 

294 ... value = 1 

295 ... extra = dict() 

296 

297 >>> samples = SamplesMixin(samples=2) 

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

299 (None, None) 

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

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

302 True 

303 

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

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

306 True 

307 

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

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

310 >>> value 

311 [1, 1] 

312 

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

314 True 

315 ''' 

316 

317 def __init__(self, samples=datetime.timedelta(seconds=2), key_prefix=None, 

318 **kwargs): 

319 self.samples = samples 

320 self.key_prefix = (self.__class__.__name__ or key_prefix) + '_' 

321 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

322 

323 def get_sample_times(self, progress, data): 

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

325 

326 def get_sample_values(self, progress, data): 

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

328 

329 def __call__(self, progress, data, delta=False): 

330 sample_times = self.get_sample_times(progress, data) 

331 sample_values = self.get_sample_values(progress, data) 

332 

333 if sample_times: 

334 sample_time = sample_times[-1] 

335 else: 

336 sample_time = datetime.datetime.min 

337 

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

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

340 sample_times.append(progress.last_update_time) 

341 sample_values.append(progress.value) 

342 

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

344 minimum_time = progress.last_update_time - self.samples 

345 minimum_value = sample_values[-1] 

346 while (sample_times[2:] and 

347 minimum_time > sample_times[1] and 

348 minimum_value > sample_values[1]): 

349 sample_times.pop(0) 

350 sample_values.pop(0) 

351 else: 

352 if len(sample_times) > self.samples: 

353 sample_times.pop(0) 

354 sample_values.pop(0) 

355 

356 if delta: 

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

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

359 if delta_time: 

360 return delta_time, delta_value 

361 else: 

362 return None, None 

363 else: 

364 return sample_times, sample_values 

365 

366 

367class ETA(Timer): 

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

369 

370 def __init__( 

371 self, 

372 format_not_started='ETA: --:--:--', 

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

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

375 format_zero='ETA: 00:00:00', 

376 format_NA='ETA: N/A', 

377 **kwargs): 

378 

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

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

381 

382 Timer.__init__(self, **kwargs) 

383 self.format_not_started = format_not_started 

384 self.format_finished = format_finished 

385 self.format = format 

386 self.format_zero = format_zero 

387 self.format_NA = format_NA 

388 

389 def _calculate_eta(self, progress, data, value, elapsed): 

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

391 if elapsed: 

392 # The max() prevents zero division errors 

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

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

395 eta_seconds = remaining * per_item 

396 else: 

397 eta_seconds = 0 

398 

399 return eta_seconds 

400 

401 def __call__(self, progress, data, value=None, elapsed=None): 

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

403 if value is None: 

404 value = data['value'] 

405 

406 if elapsed is None: 

407 elapsed = data['time_elapsed'] 

408 

409 ETA_NA = False 

410 try: 

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

412 progress, data, value=value, elapsed=elapsed) 

413 except TypeError: 

414 data['eta_seconds'] = None 

415 ETA_NA = True 

416 

417 data['eta'] = None 

418 if data['eta_seconds']: 

419 try: 

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

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

422 pass 

423 

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

425 format = self.format_not_started 

426 elif progress.end_time: 

427 format = self.format_finished 

428 elif data['eta']: 

429 format = self.format 

430 elif ETA_NA: 

431 format = self.format_NA 

432 else: 

433 format = self.format_zero 

434 

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

436 

437 

438class AbsoluteETA(ETA): 

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

440 

441 def _calculate_eta(self, progress, data, value, elapsed): 

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

443 now = datetime.datetime.now() 

444 try: 

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

446 except OverflowError: # pragma: no cover 

447 return datetime.datetime.max 

448 

449 def __init__( 

450 self, 

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

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

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

454 **kwargs): 

455 ETA.__init__(self, format_not_started=format_not_started, 

456 format_finished=format_finished, format=format, **kwargs) 

457 

458 

459class AdaptiveETA(ETA, SamplesMixin): 

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

461 

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

463 Very convenient for resuming the progress halfway. 

464 ''' 

465 

466 def __init__(self, **kwargs): 

467 ETA.__init__(self, **kwargs) 

468 SamplesMixin.__init__(self, **kwargs) 

469 

470 def __call__(self, progress, data): 

471 elapsed, value = SamplesMixin.__call__(self, progress, data, 

472 delta=True) 

473 if not elapsed: 

474 value = None 

475 elapsed = 0 

476 

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

478 

479 

480class DataSize(FormatWidgetMixin, WidgetBase): 

481 ''' 

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

483 

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

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

486 ''' 

487 

488 def __init__( 

489 self, variable='value', 

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

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

492 **kwargs): 

493 self.variable = variable 

494 self.unit = unit 

495 self.prefixes = prefixes 

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

497 WidgetBase.__init__(self, **kwargs) 

498 

499 def __call__(self, progress, data): 

500 value = data[self.variable] 

501 if value is not None: 

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

503 else: 

504 scaled = power = 0 

505 

506 data['scaled'] = scaled 

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

508 data['unit'] = self.unit 

509 

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

511 

512 

513class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): 

514 ''' 

515 WidgetBase for showing the transfer speed (useful for file transfers). 

516 ''' 

517 

518 def __init__( 

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

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

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

522 **kwargs): 

523 self.unit = unit 

524 self.prefixes = prefixes 

525 self.inverse_format = inverse_format 

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

527 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

528 

529 def _speed(self, value, elapsed): 

530 speed = float(value) / elapsed 

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

532 

533 def __call__(self, progress, data, value=None, total_seconds_elapsed=None): 

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

535 if value is None: 

536 value = data['value'] 

537 

538 elapsed = utils.deltas_to_seconds( 

539 total_seconds_elapsed, 

540 data['total_seconds_elapsed']) 

541 

542 if value is not None and elapsed is not None \ 

543 and elapsed > 2e-6 and value > 2e-6: # =~ 0 

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

545 else: 

546 scaled = power = 0 

547 

548 data['unit'] = self.unit 

549 if power == 0 and scaled < 0.1: 

550 if scaled > 0: 

551 scaled = 1 / scaled 

552 data['scaled'] = scaled 

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

554 return FormatWidgetMixin.__call__(self, progress, data, 

555 self.inverse_format) 

556 else: 

557 data['scaled'] = scaled 

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

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

560 

561 

562class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): 

563 '''WidgetBase for showing the transfer speed, based on the last X samples 

564 ''' 

565 

566 def __init__(self, **kwargs): 

567 FileTransferSpeed.__init__(self, **kwargs) 

568 SamplesMixin.__init__(self, **kwargs) 

569 

570 def __call__(self, progress, data): 

571 elapsed, value = SamplesMixin.__call__(self, progress, data, 

572 delta=True) 

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

574 

575 

576class AnimatedMarker(TimeSensitiveWidgetBase): 

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

578 it were rotating. 

579 ''' 

580 

581 def __init__(self, markers='|/-\\', default=None, fill='', 

582 marker_wrap=None, fill_wrap=None, **kwargs): 

583 self.markers = markers 

584 self.marker_wrap = create_wrapper(marker_wrap) 

585 self.default = default or markers[0] 

586 self.fill_wrap = create_wrapper(fill_wrap) 

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

588 WidgetBase.__init__(self, **kwargs) 

589 

590 def __call__(self, progress, data, width=None): 

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

592 finished''' 

593 

594 if progress.end_time: 

595 return self.default 

596 

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

598 if self.marker_wrap: 

599 marker = self.marker_wrap.format(marker) 

600 

601 if self.fill: 

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

603 fill = self.fill(progress, data, width - progress.custom_len( 

604 marker)) 

605 else: 

606 fill = '' 

607 

608 # Python 3 returns an int when indexing bytes 

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

610 marker = bytes(marker) 

611 fill = fill.encode() 

612 else: 

613 # cast fill to the same type as marker 

614 fill = type(marker)(fill) 

615 

616 return fill + marker 

617 

618 

619# Alias for backwards compatibility 

620RotatingMarker = AnimatedMarker 

621 

622 

623class Counter(FormatWidgetMixin, WidgetBase): 

624 '''Displays the current count''' 

625 

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

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

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

629 

630 def __call__(self, progress, data, format=None): 

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

632 

633 

634class Percentage(FormatWidgetMixin, WidgetBase): 

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

636 

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

638 self.na = na 

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

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

641 

642 def get_format(self, progress, data, format=None): 

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

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

645 if not percentage and percentage != 0: 

646 return self.na 

647 

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

649 

650 

651class SimpleProgress(FormatWidgetMixin, WidgetBase): 

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

653 

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

655 

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

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

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

659 self.max_width_cache = dict(default=self.max_width) 

660 

661 def __call__(self, progress, data, format=None): 

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

663 if data.get('max_value'): 

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

665 else: 

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

667 

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

669 if data.get('value'): 

670 data['value_s'] = data['value'] 

671 else: 

672 data['value_s'] = 0 

673 

674 formatted = FormatWidgetMixin.__call__(self, progress, data, 

675 format=format) 

676 

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

678 key = progress.min_value, progress.max_value 

679 max_width = self.max_width_cache.get(key, self.max_width) 

680 if not max_width: 

681 temporary_data = data.copy() 

682 for value in key: 

683 if value is None: # pragma: no cover 

684 continue 

685 

686 temporary_data['value'] = value 

687 width = progress.custom_len(FormatWidgetMixin.__call__( 

688 self, progress, temporary_data, format=format)) 

689 if width: # pragma: no branch 

690 max_width = max(max_width or 0, width) 

691 

692 self.max_width_cache[key] = max_width 

693 

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

695 if max_width: # pragma: no branch 

696 formatted = formatted.rjust(max_width) 

697 

698 return formatted 

699 

700 

701class Bar(AutoWidthWidgetBase): 

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

703 

704 def __init__(self, marker='#', left='|', right='|', fill=' ', 

705 fill_left=True, marker_wrap=None, **kwargs): 

706 '''Creates a customizable progress bar. 

707 

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

709 

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

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

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

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

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

715 ''' 

716 

717 self.marker = create_marker(marker, marker_wrap) 

718 self.left = string_or_lambda(left) 

719 self.right = string_or_lambda(right) 

720 self.fill = string_or_lambda(fill) 

721 self.fill_left = fill_left 

722 

723 AutoWidthWidgetBase.__init__(self, **kwargs) 

724 

725 def __call__(self, progress, data, width): 

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

727 

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

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

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

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

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

733 

734 # Make sure we ignore invisible characters when filling 

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

736 

737 if self.fill_left: 

738 marker = marker.ljust(width, fill) 

739 else: 

740 marker = marker.rjust(width, fill) 

741 

742 return left + marker + right 

743 

744 

745class ReverseBar(Bar): 

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

747 

748 def __init__(self, marker='#', left='|', right='|', fill=' ', 

749 fill_left=False, **kwargs): 

750 '''Creates a customizable progress bar. 

751 

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

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

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

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

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

757 ''' 

758 Bar.__init__(self, marker=marker, left=left, right=right, fill=fill, 

759 fill_left=fill_left, **kwargs) 

760 

761 

762class BouncingBar(Bar, TimeSensitiveWidgetBase): 

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

764 

765 INTERVAL = datetime.timedelta(milliseconds=100) 

766 

767 def __call__(self, progress, data, width): 

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

769 

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

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

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

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

774 

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

776 

777 if width: # pragma: no branch 

778 value = int( 

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

780 

781 a = value % width 

782 b = width - a - 1 

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

784 a, b = b, a 

785 

786 if self.fill_left: 

787 marker = a * fill + marker + b * fill 

788 else: 

789 marker = b * fill + marker + a * fill 

790 

791 return left + marker + right 

792 

793 

794class FormatCustomText(FormatWidgetMixin, WidgetBase): 

795 mapping = {} 

796 copy = False 

797 

798 def __init__(self, format, mapping=mapping, **kwargs): 

799 self.format = format 

800 self.mapping = mapping 

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

802 WidgetBase.__init__(self, **kwargs) 

803 

804 def update_mapping(self, **mapping): 

805 self.mapping.update(mapping) 

806 

807 def __call__(self, progress, data): 

808 return FormatWidgetMixin.__call__( 

809 self, progress, self.mapping, self.format) 

810 

811 

812class VariableMixin(object): 

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

814 

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

816 if not isinstance(name, str): 

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

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

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

820 self.name = name 

821 

822 

823class MultiRangeBar(Bar, VariableMixin): 

824 ''' 

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

826 

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

828 

829 .. code-block:: python 

830 

831 [ 

832 ['Symbol1', amount1], 

833 ['Symbol2', amount2], 

834 ... 

835 ] 

836 ''' 

837 

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

839 VariableMixin.__init__(self, name) 

840 Bar.__init__(self, **kwargs) 

841 self.markers = [ 

842 string_or_lambda(marker) 

843 for marker in markers 

844 ] 

845 

846 def get_values(self, progress, data): 

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

848 

849 def __call__(self, progress, data, width): 

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

851 

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

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

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

855 values = self.get_values(progress, data) 

856 

857 values_sum = sum(values) 

858 if width and values_sum: 

859 middle = '' 

860 values_accumulated = 0 

861 width_accumulated = 0 

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

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

864 assert progress.custom_len(marker) == 1 

865 

866 values_accumulated += value 

867 item_width = int(values_accumulated / values_sum * width) 

868 item_width -= width_accumulated 

869 width_accumulated += item_width 

870 middle += item_width * marker 

871 else: 

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

873 assert progress.custom_len(fill) == 1 

874 middle = fill * width 

875 

876 return left + middle + right 

877 

878 

879class MultiProgressBar(MultiRangeBar): 

880 def __init__(self, 

881 name, 

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

883 # terminals don't show the characters correctly! 

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

885 **kwargs): 

886 MultiRangeBar.__init__(self, name=name, 

887 markers=list(reversed(markers)), **kwargs) 

888 

889 def get_values(self, progress, data): 

890 ranges = [0] * len(self.markers) 

891 for progress in data['variables'][self.name] or []: 

892 if not isinstance(progress, (int, float)): 

893 # Progress is (value, max) 

894 progress_value, progress_max = progress 

895 progress = float(progress_value) / float(progress_max) 

896 

897 if progress < 0 or progress > 1: 

898 raise ValueError( 

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

900 progress) 

901 

902 range_ = progress * (len(ranges) - 1) 

903 pos = int(range_) 

904 frac = range_ % 1 

905 ranges[pos] += (1 - frac) 

906 if (frac): 

907 ranges[pos + 1] += (frac) 

908 

909 if self.fill_left: 

910 ranges = list(reversed(ranges)) 

911 return ranges 

912 

913 

914class GranularMarkers: 

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

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

917 snake = ' ▖▌▛█' 

918 fade_in = ' ░▒▓█' 

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

920 growing_circles = ' .oO' 

921 

922 

923class GranularBar(AutoWidthWidgetBase): 

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

925 by using multiple marker characters. 

926 

927 Examples of markers: 

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

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

930 - Snake: ` ▖▌▛█` 

931 - Fade in: ` ░▒▓█` 

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

933 - Growing circles: ` .oO` 

934 

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

936 for example 

937 ''' 

938 

939 def __init__(self, markers=GranularMarkers.smooth, left='|', right='|', 

940 **kwargs): 

941 '''Creates a customizable progress bar. 

942 

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

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

945 Ex: ` .oO`. 

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

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

948 ''' 

949 self.markers = markers 

950 self.left = string_or_lambda(left) 

951 self.right = string_or_lambda(right) 

952 

953 AutoWidthWidgetBase.__init__(self, **kwargs) 

954 

955 def __call__(self, progress, data, width): 

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

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

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

959 

960 if progress.max_value is not base.UnknownLength \ 

961 and progress.max_value > 0: 

962 percent = progress.value / progress.max_value 

963 else: 

964 percent = 0 

965 

966 num_chars = percent * width 

967 

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

969 

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

971 if marker_idx: 

972 marker += self.markers[marker_idx] 

973 

974 marker = converters.to_unicode(marker) 

975 

976 # Make sure we ignore invisible characters when filling 

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

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

979 

980 return left + marker + right 

981 

982 

983class FormatLabelBar(FormatLabel, Bar): 

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

985 

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

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

988 Bar.__init__(self, **kwargs) 

989 

990 def __call__(self, progress, data, width, format=None): 

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

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

993 

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

995 center_len = progress.custom_len(center) 

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

997 center_right = center_left + center_len 

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

999 

1000 

1001class PercentageLabelBar(Percentage, FormatLabelBar): 

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

1003 

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

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

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

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

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

1009 

1010 

1011class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 

1012 '''Displays a custom variable.''' 

1013 

1014 def __init__(self, name, format='{name}: {formatted_value}', 

1015 width=6, precision=3, **kwargs): 

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

1017 self.format = format 

1018 self.width = width 

1019 self.precision = precision 

1020 VariableMixin.__init__(self, name=name) 

1021 WidgetBase.__init__(self, **kwargs) 

1022 

1023 def __call__(self, progress, data): 

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

1025 context = data.copy() 

1026 context['value'] = value 

1027 context['name'] = self.name 

1028 context['width'] = self.width 

1029 context['precision'] = self.precision 

1030 

1031 try: 

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

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

1034 value = float(value) 

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

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

1037 except (TypeError, ValueError): 

1038 if value: 

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

1040 **context) 

1041 else: 

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

1043 

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

1045 

1046 

1047class DynamicMessage(Variable): 

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

1049 pass 

1050 

1051 

1052class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 

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

1054 INTERVAL = datetime.timedelta(seconds=1) 

1055 

1056 def __init__(self, format='Current Time: %(current_time)s', 

1057 microseconds=False, **kwargs): 

1058 self.microseconds = microseconds 

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

1060 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

1061 

1062 def __call__(self, progress, data): 

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

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

1065 

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

1067 

1068 def current_datetime(self): 

1069 now = datetime.datetime.now() 

1070 if not self.microseconds: 

1071 now = now.replace(microsecond=0) 

1072 

1073 return now 

1074 

1075 def current_time(self): 

1076 return self.current_datetime().time()