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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

503 statements  

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 FormatLabel.__init__(self, format=format, **kwargs) 

276 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

277 

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

279 format_time = staticmethod(utils.format_time) 

280 

281 

282class SamplesMixin(TimeSensitiveWidgetBase): 

283 ''' 

284 Mixing for widgets that average multiple measurements 

285 

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

287 certain amount of time 

288 

289 >>> class progress: 

290 ... last_update_time = datetime.datetime.now() 

291 ... value = 1 

292 ... extra = dict() 

293 

294 >>> samples = SamplesMixin(samples=2) 

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

296 (None, None) 

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

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

299 True 

300 

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

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

303 True 

304 

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

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

307 >>> value 

308 [1, 1] 

309 

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

311 True 

312 ''' 

313 

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

315 **kwargs): 

316 self.samples = samples 

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

318 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

319 

320 def get_sample_times(self, progress, data): 

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

322 

323 def get_sample_values(self, progress, data): 

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

325 

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

327 sample_times = self.get_sample_times(progress, data) 

328 sample_values = self.get_sample_values(progress, data) 

329 

330 if sample_times: 

331 sample_time = sample_times[-1] 

332 else: 

333 sample_time = datetime.datetime.min 

334 

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

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

337 sample_times.append(progress.last_update_time) 

338 sample_values.append(progress.value) 

339 

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

341 minimum_time = progress.last_update_time - self.samples 

342 minimum_value = sample_values[-1] 

343 while (sample_times[2:] and 

344 minimum_time > sample_times[1] and 

345 minimum_value > sample_values[1]): 

346 sample_times.pop(0) 

347 sample_values.pop(0) 

348 else: 

349 if len(sample_times) > self.samples: 

350 sample_times.pop(0) 

351 sample_values.pop(0) 

352 

353 if delta: 

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

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

356 if delta_time: 

357 return delta_time, delta_value 

358 else: 

359 return None, None 

360 else: 

361 return sample_times, sample_values 

362 

363 

364class ETA(Timer): 

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

366 

367 def __init__( 

368 self, 

369 format_not_started='ETA: --:--:--', 

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

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

372 format_zero='ETA: 00:00:00', 

373 format_NA='ETA: N/A', 

374 **kwargs): 

375 

376 Timer.__init__(self, **kwargs) 

377 self.format_not_started = format_not_started 

378 self.format_finished = format_finished 

379 self.format = format 

380 self.format_zero = format_zero 

381 self.format_NA = format_NA 

382 

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

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

385 if elapsed: 

386 # The max() prevents zero division errors 

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

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

389 eta_seconds = remaining * per_item 

390 else: 

391 eta_seconds = 0 

392 

393 return eta_seconds 

394 

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

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

397 if value is None: 

398 value = data['value'] 

399 

400 if elapsed is None: 

401 elapsed = data['time_elapsed'] 

402 

403 ETA_NA = False 

404 try: 

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

406 progress, data, value=value, elapsed=elapsed) 

407 except TypeError: 

408 data['eta_seconds'] = None 

409 ETA_NA = True 

410 

411 data['eta'] = None 

412 if data['eta_seconds']: 

413 try: 

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

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

416 pass 

417 

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

419 format = self.format_not_started 

420 elif progress.end_time: 

421 format = self.format_finished 

422 elif data['eta']: 

423 format = self.format 

424 elif ETA_NA: 

425 format = self.format_NA 

426 else: 

427 format = self.format_zero 

428 

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

430 

431 

432class AbsoluteETA(ETA): 

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

434 

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

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

437 now = datetime.datetime.now() 

438 try: 

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

440 except OverflowError: # pragma: no cover 

441 return datetime.datetime.max 

442 

443 def __init__( 

444 self, 

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

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

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

448 **kwargs): 

449 ETA.__init__(self, format_not_started=format_not_started, 

450 format_finished=format_finished, format=format, **kwargs) 

451 

452 

453class AdaptiveETA(ETA, SamplesMixin): 

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

455 

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

457 Very convenient for resuming the progress halfway. 

458 ''' 

459 

460 def __init__(self, **kwargs): 

461 ETA.__init__(self, **kwargs) 

462 SamplesMixin.__init__(self, **kwargs) 

463 

464 def __call__(self, progress, data): 

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

466 delta=True) 

467 if not elapsed: 

468 value = None 

469 elapsed = 0 

470 

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

472 

473 

474class DataSize(FormatWidgetMixin, WidgetBase): 

475 ''' 

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

477 

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

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

480 ''' 

481 

482 def __init__( 

483 self, variable='value', 

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

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

486 **kwargs): 

487 self.variable = variable 

488 self.unit = unit 

489 self.prefixes = prefixes 

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

491 WidgetBase.__init__(self, **kwargs) 

492 

493 def __call__(self, progress, data): 

494 value = data[self.variable] 

495 if value is not None: 

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

497 else: 

498 scaled = power = 0 

499 

500 data['scaled'] = scaled 

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

502 data['unit'] = self.unit 

503 

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

505 

506 

507class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): 

508 ''' 

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

510 ''' 

511 

512 def __init__( 

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

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

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

516 **kwargs): 

517 self.unit = unit 

518 self.prefixes = prefixes 

519 self.inverse_format = inverse_format 

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

521 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

522 

523 def _speed(self, value, elapsed): 

524 speed = float(value) / elapsed 

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

526 

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

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

529 if value is None: 

530 value = data['value'] 

531 

532 elapsed = utils.deltas_to_seconds( 

533 total_seconds_elapsed, 

534 data['total_seconds_elapsed']) 

535 

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

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

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

539 else: 

540 scaled = power = 0 

541 

542 data['unit'] = self.unit 

543 if power == 0 and scaled < 0.1: 

544 if scaled > 0: 

545 scaled = 1 / scaled 

546 data['scaled'] = scaled 

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

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

549 self.inverse_format) 

550 else: 

551 data['scaled'] = scaled 

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

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

554 

555 

556class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): 

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

558 ''' 

559 

560 def __init__(self, **kwargs): 

561 FileTransferSpeed.__init__(self, **kwargs) 

562 SamplesMixin.__init__(self, **kwargs) 

563 

564 def __call__(self, progress, data): 

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

566 delta=True) 

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

568 

569 

570class AnimatedMarker(TimeSensitiveWidgetBase): 

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

572 it were rotating. 

573 ''' 

574 

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

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

577 self.markers = markers 

578 self.marker_wrap = create_wrapper(marker_wrap) 

579 self.default = default or markers[0] 

580 self.fill_wrap = create_wrapper(fill_wrap) 

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

582 WidgetBase.__init__(self, **kwargs) 

583 

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

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

586 finished''' 

587 

588 if progress.end_time: 

589 return self.default 

590 

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

592 if self.marker_wrap: 

593 marker = self.marker_wrap.format(marker) 

594 

595 if self.fill: 

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

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

598 marker)) 

599 else: 

600 fill = '' 

601 

602 # Python 3 returns an int when indexing bytes 

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

604 marker = bytes(marker) 

605 fill = fill.encode() 

606 else: 

607 # cast fill to the same type as marker 

608 fill = type(marker)(fill) 

609 

610 return fill + marker 

611 

612 

613# Alias for backwards compatibility 

614RotatingMarker = AnimatedMarker 

615 

616 

617class Counter(FormatWidgetMixin, WidgetBase): 

618 '''Displays the current count''' 

619 

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

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

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

623 

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

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

626 

627 

628class Percentage(FormatWidgetMixin, WidgetBase): 

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

630 

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

632 self.na = na 

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

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

635 

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

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

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

639 if not percentage and percentage != 0: 

640 return self.na 

641 

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

643 

644 

645class SimpleProgress(FormatWidgetMixin, WidgetBase): 

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

647 

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

649 

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

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

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

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

654 

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

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

657 if data.get('max_value'): 

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

659 else: 

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

661 

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

663 if data.get('value'): 

664 data['value_s'] = data['value'] 

665 else: 

666 data['value_s'] = 0 

667 

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

669 format=format) 

670 

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

672 key = progress.min_value, progress.max_value 

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

674 if not max_width: 

675 temporary_data = data.copy() 

676 for value in key: 

677 if value is None: # pragma: no cover 

678 continue 

679 

680 temporary_data['value'] = value 

681 width = progress.custom_len(FormatWidgetMixin.__call__( 

682 self, progress, temporary_data, format=format)) 

683 if width: # pragma: no branch 

684 max_width = max(max_width or 0, width) 

685 

686 self.max_width_cache[key] = max_width 

687 

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

689 if max_width: # pragma: no branch 

690 formatted = formatted.rjust(max_width) 

691 

692 return formatted 

693 

694 

695class Bar(AutoWidthWidgetBase): 

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

697 

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

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

700 '''Creates a customizable progress bar. 

701 

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

703 

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

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

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

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

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

709 ''' 

710 

711 self.marker = create_marker(marker, marker_wrap) 

712 self.left = string_or_lambda(left) 

713 self.right = string_or_lambda(right) 

714 self.fill = string_or_lambda(fill) 

715 self.fill_left = fill_left 

716 

717 AutoWidthWidgetBase.__init__(self, **kwargs) 

718 

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

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

721 

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

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

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

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

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

727 

728 # Make sure we ignore invisible characters when filling 

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

730 

731 if self.fill_left: 

732 marker = marker.ljust(width, fill) 

733 else: 

734 marker = marker.rjust(width, fill) 

735 

736 return left + marker + right 

737 

738 

739class ReverseBar(Bar): 

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

741 

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

743 fill_left=False, **kwargs): 

744 '''Creates a customizable progress bar. 

745 

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

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

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

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

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

751 ''' 

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

753 fill_left=fill_left, **kwargs) 

754 

755 

756class BouncingBar(Bar, TimeSensitiveWidgetBase): 

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

758 

759 INTERVAL = datetime.timedelta(milliseconds=100) 

760 

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

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

763 

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

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

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

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

768 

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

770 

771 if width: # pragma: no branch 

772 value = int( 

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

774 

775 a = value % width 

776 b = width - a - 1 

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

778 a, b = b, a 

779 

780 if self.fill_left: 

781 marker = a * fill + marker + b * fill 

782 else: 

783 marker = b * fill + marker + a * fill 

784 

785 return left + marker + right 

786 

787 

788class FormatCustomText(FormatWidgetMixin, WidgetBase): 

789 mapping = {} 

790 copy = False 

791 

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

793 self.format = format 

794 self.mapping = mapping 

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

796 WidgetBase.__init__(self, **kwargs) 

797 

798 def update_mapping(self, **mapping): 

799 self.mapping.update(mapping) 

800 

801 def __call__(self, progress, data): 

802 return FormatWidgetMixin.__call__( 

803 self, progress, self.mapping, self.format) 

804 

805 

806class VariableMixin(object): 

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

808 

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

810 if not isinstance(name, str): 

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

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

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

814 self.name = name 

815 

816 

817class MultiRangeBar(Bar, VariableMixin): 

818 ''' 

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

820 

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

822 

823 .. code-block:: python 

824 

825 [ 

826 ['Symbol1', amount1], 

827 ['Symbol2', amount2], 

828 ... 

829 ] 

830 ''' 

831 

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

833 VariableMixin.__init__(self, name) 

834 Bar.__init__(self, **kwargs) 

835 self.markers = [ 

836 string_or_lambda(marker) 

837 for marker in markers 

838 ] 

839 

840 def get_values(self, progress, data): 

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

842 

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

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

845 

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

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

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

849 values = self.get_values(progress, data) 

850 

851 values_sum = sum(values) 

852 if width and values_sum: 

853 middle = '' 

854 values_accumulated = 0 

855 width_accumulated = 0 

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

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

858 assert progress.custom_len(marker) == 1 

859 

860 values_accumulated += value 

861 item_width = int(values_accumulated / values_sum * width) 

862 item_width -= width_accumulated 

863 width_accumulated += item_width 

864 middle += item_width * marker 

865 else: 

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

867 assert progress.custom_len(fill) == 1 

868 middle = fill * width 

869 

870 return left + middle + right 

871 

872 

873class MultiProgressBar(MultiRangeBar): 

874 def __init__(self, 

875 name, 

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

877 # terminals don't show the characters correctly! 

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

879 **kwargs): 

880 MultiRangeBar.__init__(self, name=name, 

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

882 

883 def get_values(self, progress, data): 

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

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

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

887 # Progress is (value, max) 

888 progress_value, progress_max = progress 

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

890 

891 if progress < 0 or progress > 1: 

892 raise ValueError( 

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

894 progress) 

895 

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

897 pos = int(range_) 

898 frac = range_ % 1 

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

900 if (frac): 

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

902 

903 if self.fill_left: 

904 ranges = list(reversed(ranges)) 

905 return ranges 

906 

907 

908class GranularMarkers: 

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

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

911 snake = ' ▖▌▛█' 

912 fade_in = ' ░▒▓█' 

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

914 growing_circles = ' .oO' 

915 

916 

917class GranularBar(AutoWidthWidgetBase): 

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

919 by using multiple marker characters. 

920 

921 Examples of markers: 

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

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

924 - Snake: ` ▖▌▛█` 

925 - Fade in: ` ░▒▓█` 

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

927 - Growing circles: ` .oO` 

928 

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

930 for example 

931 ''' 

932 

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

934 **kwargs): 

935 '''Creates a customizable progress bar. 

936 

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

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

939 Ex: ` .oO`. 

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

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

942 ''' 

943 self.markers = markers 

944 self.left = string_or_lambda(left) 

945 self.right = string_or_lambda(right) 

946 

947 AutoWidthWidgetBase.__init__(self, **kwargs) 

948 

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

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

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

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

953 

954 if progress.max_value is not base.UnknownLength \ 

955 and progress.max_value > 0: 

956 percent = progress.value / progress.max_value 

957 else: 

958 percent = 0 

959 

960 num_chars = percent * width 

961 

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

963 

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

965 if marker_idx: 

966 marker += self.markers[marker_idx] 

967 

968 marker = converters.to_unicode(marker) 

969 

970 # Make sure we ignore invisible characters when filling 

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

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

973 

974 return left + marker + right 

975 

976 

977class FormatLabelBar(FormatLabel, Bar): 

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

979 

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

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

982 Bar.__init__(self, **kwargs) 

983 

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

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

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

987 

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

989 center_len = progress.custom_len(center) 

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

991 center_right = center_left + center_len 

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

993 

994 

995class PercentageLabelBar(Percentage, FormatLabelBar): 

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

997 

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

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

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

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

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

1003 

1004 

1005class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 

1006 '''Displays a custom variable.''' 

1007 

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

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

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

1011 self.format = format 

1012 self.width = width 

1013 self.precision = precision 

1014 VariableMixin.__init__(self, name=name) 

1015 WidgetBase.__init__(self, **kwargs) 

1016 

1017 def __call__(self, progress, data): 

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

1019 context = data.copy() 

1020 context['value'] = value 

1021 context['name'] = self.name 

1022 context['width'] = self.width 

1023 context['precision'] = self.precision 

1024 

1025 try: 

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

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

1028 value = float(value) 

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

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

1031 except (TypeError, ValueError): 

1032 if value: 

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

1034 **context) 

1035 else: 

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

1037 

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

1039 

1040 

1041class DynamicMessage(Variable): 

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

1043 pass 

1044 

1045 

1046class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 

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

1048 INTERVAL = datetime.timedelta(seconds=1) 

1049 

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

1051 microseconds=False, **kwargs): 

1052 self.microseconds = microseconds 

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

1054 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

1055 

1056 def __call__(self, progress, data): 

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

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

1059 

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

1061 

1062 def current_datetime(self): 

1063 now = datetime.datetime.now() 

1064 if not self.microseconds: 

1065 now = now.replace(microsecond=0) 

1066 

1067 return now 

1068 

1069 def current_time(self): 

1070 return self.current_datetime().time()