Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

2from __future__ import absolute_import 

3from __future__ import division 

4from __future__ import print_function 

5from __future__ import unicode_literals 

6from __future__ import with_statement 

7 

8import abc 

9import sys 

10import pprint 

11import datetime 

12import functools 

13 

14from python_utils import converters 

15 

16import six 

17 

18from . import base 

19from . import utils 

20 

21MAX_DATE = datetime.date.max 

22MAX_TIME = datetime.time.max 

23MAX_DATETIME = datetime.datetime.max 

24 

25 

26def string_or_lambda(input_): 

27 if isinstance(input_, six.string_types): 

28 def render_input(progress, data, width): 

29 return input_ % data 

30 

31 return render_input 

32 else: 

33 return input_ 

34 

35 

36def create_wrapper(wrapper): 

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

38 

39 >>> create_wrapper('') 

40 

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

42 a{}b 

43 

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

45 a{}b 

46 ''' 

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

48 a, b = wrapper 

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

50 elif not wrapper: 

51 return 

52 

53 if isinstance(wrapper, six.string_types): 

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

55 else: 

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

57 ' template string with {}') 

58 

59 return wrapper 

60 

61 

62def wrapper(function, wrapper): 

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

64 begin/end strings 

65 

66 ''' 

67 wrapper = create_wrapper(wrapper) 

68 if not wrapper: 

69 return function 

70 

71 @functools.wraps(function) 

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

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

74 

75 return wrap 

76 

77 

78def create_marker(marker, wrap=None): 

79 def _marker(progress, data, width): 

80 if progress.max_value is not base.UnknownLength \ 

81 and progress.max_value > 0: 

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

83 return (marker * length) 

84 else: 

85 return marker 

86 

87 if isinstance(marker, six.string_types): 

88 marker = converters.to_unicode(marker) 

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

90 'Markers are required to be 1 char' 

91 return wrapper(_marker, wrap) 

92 else: 

93 return wrapper(marker, wrap) 

94 

95 

96class FormatWidgetMixin(object): 

97 '''Mixin to format widgets using a formatstring 

98 

99 Variables available: 

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

101 - value: The current value 

102 - total_seconds_elapsed: The seconds since the bar started 

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

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

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

106 - days_elapsed: The hours since the bar started 

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

108 days 

109 - percentage: Percentage as a float 

110 ''' 

111 required_values = [] 

112 

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

114 self.new_style = new_style 

115 self.format = format 

116 

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

118 return format or self.format 

119 

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

121 '''Formats the widget into a string''' 

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

123 try: 

124 if self.new_style: 

125 return format.format(**data) 

126 else: 

127 return format % data 

128 except (TypeError, KeyError): 

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

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

131 raise 

132 

133 

134class WidthWidgetMixin(object): 

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

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

137 screens.. 

138 

139 Variables available: 

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

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

142 

143 >>> class Progress(object): 

144 ... term_width = 0 

145 

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

147 False 

148 >>> Progress.term_width = 5 

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

150 True 

151 >>> Progress.term_width = 10 

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

153 True 

154 >>> Progress.term_width = 11 

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

156 False 

157 ''' 

158 

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

160 self.min_width = min_width 

161 self.max_width = max_width 

162 

163 def check_size(self, progress): 

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

165 return False 

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

167 return False 

168 else: 

169 return True 

170 

171 

172class WidgetBase(WidthWidgetMixin): 

173 __metaclass__ = abc.ABCMeta 

174 '''The base class for all widgets 

175 

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

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

178 display incorrectly if the size changes drastically and repeatedly. 

179 

180 The boolean INTERVAL informs the ProgressBar that it should be 

181 updated more often because it is time sensitive. 

182 

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

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

185 screens. 

186 

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

188 information specific to a progressbar should be stored within the 

189 progressbar instead of the widget. 

190 

191 Variables available: 

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

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

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

195 with a lower one 

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

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

198 require the shared state so this needs to be optional 

199 ''' 

200 copy = True 

201 

202 @abc.abstractmethod 

203 def __call__(self, progress, data): 

204 '''Updates the widget. 

205 

206 progress - a reference to the calling ProgressBar 

207 ''' 

208 

209 

210class AutoWidthWidgetBase(WidgetBase): 

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

212 

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

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

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

216 ''' 

217 

218 @abc.abstractmethod 

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

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

221 

222 progress - a reference to the calling ProgressBar 

223 width - The total width the widget must fill 

224 ''' 

225 

226 

227class TimeSensitiveWidgetBase(WidgetBase): 

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

229 

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

231 every `INTERVAL` 

232 ''' 

233 INTERVAL = datetime.timedelta(milliseconds=100) 

234 

235 

236class FormatLabel(FormatWidgetMixin, WidgetBase): 

237 '''Displays a formatted label 

238 

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

240 >>> class Progress(object): 

241 ... pass 

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

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

244 'test :: test ' 

245 

246 ''' 

247 

248 mapping = { 

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

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

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

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

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

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

255 'value': ('value', None), 

256 } 

257 

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

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

260 WidgetBase.__init__(self, **kwargs) 

261 

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

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

264 try: 

265 if transform is None: 

266 data[name] = data[key] 

267 else: 

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

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

270 pass 

271 

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

273 

274 

275class Timer(FormatLabel, TimeSensitiveWidgetBase): 

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

277 

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

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

280 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

281 

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

283 format_time = staticmethod(utils.format_time) 

284 

285 

286class SamplesMixin(TimeSensitiveWidgetBase): 

287 ''' 

288 Mixing for widgets that average multiple measurements 

289 

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

291 certain amount of time 

292 

293 >>> class progress: 

294 ... last_update_time = datetime.datetime.now() 

295 ... value = 1 

296 ... extra = dict() 

297 

298 >>> samples = SamplesMixin(samples=2) 

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

300 (None, None) 

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

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

303 True 

304 

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

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

307 True 

308 

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

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

311 >>> value 

312 [1, 1] 

313 

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

315 True 

316 ''' 

317 

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

319 **kwargs): 

320 self.samples = samples 

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

322 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

323 

324 def get_sample_times(self, progress, data): 

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

326 

327 def get_sample_values(self, progress, data): 

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

329 

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

331 sample_times = self.get_sample_times(progress, data) 

332 sample_values = self.get_sample_values(progress, data) 

333 

334 if sample_times: 

335 sample_time = sample_times[-1] 

336 else: 

337 sample_time = datetime.datetime.min 

338 

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

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

341 sample_times.append(progress.last_update_time) 

342 sample_values.append(progress.value) 

343 

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

345 minimum_time = progress.last_update_time - self.samples 

346 minimum_value = sample_values[-1] 

347 while (sample_times[2:] and 

348 minimum_time > sample_times[1] and 

349 minimum_value > sample_values[1]): 

350 sample_times.pop(0) 

351 sample_values.pop(0) 

352 else: 

353 if len(sample_times) > self.samples: 

354 sample_times.pop(0) 

355 sample_values.pop(0) 

356 

357 if delta: 

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

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

360 if delta_time: 

361 return delta_time, delta_value 

362 else: 

363 return None, None 

364 else: 

365 return sample_times, sample_values 

366 

367 

368class ETA(Timer): 

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

370 

371 def __init__( 

372 self, 

373 format_not_started='ETA: --:--:--', 

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

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

376 format_zero='ETA: 00:00:00', 

377 format_NA='ETA: N/A', 

378 **kwargs): 

379 

380 Timer.__init__(self, **kwargs) 

381 self.format_not_started = format_not_started 

382 self.format_finished = format_finished 

383 self.format = format 

384 self.format_zero = format_zero 

385 self.format_NA = format_NA 

386 

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

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

389 if elapsed: 

390 # The max() prevents zero division errors 

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

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

393 eta_seconds = remaining * per_item 

394 else: 

395 eta_seconds = 0 

396 

397 return eta_seconds 

398 

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

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

401 if value is None: 

402 value = data['value'] 

403 

404 if elapsed is None: 

405 elapsed = data['time_elapsed'] 

406 

407 ETA_NA = False 

408 try: 

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

410 progress, data, value=value, elapsed=elapsed) 

411 except TypeError: 

412 data['eta_seconds'] = None 

413 ETA_NA = True 

414 

415 data['eta'] = None 

416 if data['eta_seconds']: 

417 try: 

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

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

420 pass 

421 

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

423 format = self.format_not_started 

424 elif progress.end_time: 

425 format = self.format_finished 

426 elif data['eta']: 

427 format = self.format 

428 elif ETA_NA: 

429 format = self.format_NA 

430 else: 

431 format = self.format_zero 

432 

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

434 

435 

436class AbsoluteETA(ETA): 

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

438 

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

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

441 now = datetime.datetime.now() 

442 try: 

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

444 except OverflowError: # pragma: no cover 

445 return datetime.datetime.max 

446 

447 def __init__( 

448 self, 

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

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

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

452 **kwargs): 

453 ETA.__init__(self, format_not_started=format_not_started, 

454 format_finished=format_finished, format=format, **kwargs) 

455 

456 

457class AdaptiveETA(ETA, SamplesMixin): 

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

459 

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

461 Very convenient for resuming the progress halfway. 

462 ''' 

463 

464 def __init__(self, **kwargs): 

465 ETA.__init__(self, **kwargs) 

466 SamplesMixin.__init__(self, **kwargs) 

467 

468 def __call__(self, progress, data): 

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

470 delta=True) 

471 if not elapsed: 

472 value = None 

473 elapsed = 0 

474 

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

476 

477 

478class DataSize(FormatWidgetMixin, WidgetBase): 

479 ''' 

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

481 

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

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

484 ''' 

485 

486 def __init__( 

487 self, variable='value', 

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

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

490 **kwargs): 

491 self.variable = variable 

492 self.unit = unit 

493 self.prefixes = prefixes 

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

495 WidgetBase.__init__(self, **kwargs) 

496 

497 def __call__(self, progress, data): 

498 value = data[self.variable] 

499 if value is not None: 

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

501 else: 

502 scaled = power = 0 

503 

504 data['scaled'] = scaled 

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

506 data['unit'] = self.unit 

507 

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

509 

510 

511class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): 

512 ''' 

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

514 ''' 

515 

516 def __init__( 

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

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

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

520 **kwargs): 

521 self.unit = unit 

522 self.prefixes = prefixes 

523 self.inverse_format = inverse_format 

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

525 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

526 

527 def _speed(self, value, elapsed): 

528 speed = float(value) / elapsed 

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

530 

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

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

533 if value is None: 

534 value = data['value'] 

535 

536 elapsed = utils.deltas_to_seconds( 

537 total_seconds_elapsed, 

538 data['total_seconds_elapsed']) 

539 

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

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

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

543 else: 

544 scaled = power = 0 

545 

546 data['unit'] = self.unit 

547 if power == 0 and scaled < 0.1: 

548 if scaled > 0: 

549 scaled = 1 / scaled 

550 data['scaled'] = scaled 

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

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

553 self.inverse_format) 

554 else: 

555 data['scaled'] = scaled 

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

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

558 

559 

560class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): 

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

562 ''' 

563 

564 def __init__(self, **kwargs): 

565 FileTransferSpeed.__init__(self, **kwargs) 

566 SamplesMixin.__init__(self, **kwargs) 

567 

568 def __call__(self, progress, data): 

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

570 delta=True) 

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

572 

573 

574class AnimatedMarker(TimeSensitiveWidgetBase): 

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

576 it were rotating. 

577 ''' 

578 

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

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

581 self.markers = markers 

582 self.marker_wrap = create_wrapper(marker_wrap) 

583 self.default = default or markers[0] 

584 self.fill_wrap = create_wrapper(fill_wrap) 

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

586 WidgetBase.__init__(self, **kwargs) 

587 

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

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

590 finished''' 

591 

592 if progress.end_time: 

593 return self.default 

594 

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

596 if self.marker_wrap: 

597 marker = self.marker_wrap.format(marker) 

598 

599 if self.fill: 

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

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

602 marker)) 

603 else: 

604 fill = '' 

605 

606 # Python 3 returns an int when indexing bytes 

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

608 marker = bytes(marker) 

609 fill = fill.encode() 

610 else: 

611 # cast fill to the same type as marker 

612 fill = type(marker)(fill) 

613 

614 return fill + marker 

615 

616 

617# Alias for backwards compatibility 

618RotatingMarker = AnimatedMarker 

619 

620 

621class Counter(FormatWidgetMixin, WidgetBase): 

622 '''Displays the current count''' 

623 

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

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

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

627 

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

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

630 

631 

632class Percentage(FormatWidgetMixin, WidgetBase): 

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

634 

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

636 self.na = na 

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

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

639 

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

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

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

643 if not percentage and percentage != 0: 

644 return self.na 

645 

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

647 

648 

649class SimpleProgress(FormatWidgetMixin, WidgetBase): 

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

651 

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

653 

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

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

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

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

658 

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

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

661 if data.get('max_value'): 

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

663 else: 

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

665 

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

667 if data.get('value'): 

668 data['value_s'] = data['value'] 

669 else: 

670 data['value_s'] = 0 

671 

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

673 format=format) 

674 

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

676 key = progress.min_value, progress.max_value 

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

678 if not max_width: 

679 temporary_data = data.copy() 

680 for value in key: 

681 if value is None: # pragma: no cover 

682 continue 

683 

684 temporary_data['value'] = value 

685 width = progress.custom_len(FormatWidgetMixin.__call__( 

686 self, progress, temporary_data, format=format)) 

687 if width: # pragma: no branch 

688 max_width = max(max_width or 0, width) 

689 

690 self.max_width_cache[key] = max_width 

691 

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

693 if max_width: # pragma: no branch 

694 formatted = formatted.rjust(max_width) 

695 

696 return formatted 

697 

698 

699class Bar(AutoWidthWidgetBase): 

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

701 

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

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

704 '''Creates a customizable progress bar. 

705 

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

707 

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

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

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

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

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

713 ''' 

714 

715 self.marker = create_marker(marker, marker_wrap) 

716 self.left = string_or_lambda(left) 

717 self.right = string_or_lambda(right) 

718 self.fill = string_or_lambda(fill) 

719 self.fill_left = fill_left 

720 

721 AutoWidthWidgetBase.__init__(self, **kwargs) 

722 

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

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

725 

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

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

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

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

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

731 

732 # Make sure we ignore invisible characters when filling 

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

734 

735 if self.fill_left: 

736 marker = marker.ljust(width, fill) 

737 else: 

738 marker = marker.rjust(width, fill) 

739 

740 return left + marker + right 

741 

742 

743class ReverseBar(Bar): 

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

745 

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

747 fill_left=False, **kwargs): 

748 '''Creates a customizable progress bar. 

749 

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

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

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

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

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

755 ''' 

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

757 fill_left=fill_left, **kwargs) 

758 

759 

760class BouncingBar(Bar, TimeSensitiveWidgetBase): 

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

762 

763 INTERVAL = datetime.timedelta(milliseconds=100) 

764 

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

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

767 

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

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

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

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

772 

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

774 

775 if width: # pragma: no branch 

776 value = int( 

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

778 

779 a = value % width 

780 b = width - a - 1 

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

782 a, b = b, a 

783 

784 if self.fill_left: 

785 marker = a * fill + marker + b * fill 

786 else: 

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

788 

789 return left + marker + right 

790 

791 

792class FormatCustomText(FormatWidgetMixin, WidgetBase): 

793 mapping = {} 

794 copy = False 

795 

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

797 self.format = format 

798 self.mapping = mapping 

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

800 WidgetBase.__init__(self, **kwargs) 

801 

802 def update_mapping(self, **mapping): 

803 self.mapping.update(mapping) 

804 

805 def __call__(self, progress, data): 

806 return FormatWidgetMixin.__call__( 

807 self, progress, self.mapping, self.format) 

808 

809 

810class VariableMixin(object): 

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

812 

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

814 if not isinstance(name, six.string_types): 

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

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

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

818 self.name = name 

819 

820 

821class MultiRangeBar(Bar, VariableMixin): 

822 ''' 

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

824 

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

826 

827 .. code-block:: python 

828 

829 [ 

830 ['Symbol1', amount1], 

831 ['Symbol2', amount2], 

832 ... 

833 ] 

834 ''' 

835 

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

837 VariableMixin.__init__(self, name) 

838 Bar.__init__(self, **kwargs) 

839 self.markers = [ 

840 string_or_lambda(marker) 

841 for marker in markers 

842 ] 

843 

844 def get_values(self, progress, data): 

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

846 

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

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

849 

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

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

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

853 values = self.get_values(progress, data) 

854 

855 values_sum = sum(values) 

856 if width and values_sum: 

857 middle = '' 

858 values_accumulated = 0 

859 width_accumulated = 0 

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

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

862 assert progress.custom_len(marker) == 1 

863 

864 values_accumulated += value 

865 item_width = int(values_accumulated / values_sum * width) 

866 item_width -= width_accumulated 

867 width_accumulated += item_width 

868 middle += item_width * marker 

869 else: 

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

871 assert progress.custom_len(fill) == 1 

872 middle = fill * width 

873 

874 return left + middle + right 

875 

876 

877class MultiProgressBar(MultiRangeBar): 

878 def __init__(self, 

879 name, 

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

881 # terminals don't show the characters correctly! 

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

883 **kwargs): 

884 MultiRangeBar.__init__(self, name=name, 

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

886 

887 def get_values(self, progress, data): 

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

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

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

891 # Progress is (value, max) 

892 progress_value, progress_max = progress 

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

894 

895 if progress < 0 or progress > 1: 

896 raise ValueError( 

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

898 progress) 

899 

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

901 pos = int(range_) 

902 frac = range_ % 1 

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

904 if (frac): 

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

906 

907 if self.fill_left: 

908 ranges = list(reversed(ranges)) 

909 return ranges 

910 

911 

912class GranularMarkers: 

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

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

915 snake = ' ▖▌▛█' 

916 fade_in = ' ░▒▓█' 

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

918 growing_circles = ' .oO' 

919 

920 

921class GranularBar(AutoWidthWidgetBase): 

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

923 by using multiple marker characters. 

924 

925 Examples of markers: 

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

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

928 - Snake: ` ▖▌▛█` 

929 - Fade in: ` ░▒▓█` 

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

931 - Growing circles: ` .oO` 

932 

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

934 for example 

935 ''' 

936 

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

938 **kwargs): 

939 '''Creates a customizable progress bar. 

940 

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

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

943 Ex: ` .oO`. 

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

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

946 ''' 

947 self.markers = markers 

948 self.left = string_or_lambda(left) 

949 self.right = string_or_lambda(right) 

950 

951 AutoWidthWidgetBase.__init__(self, **kwargs) 

952 

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

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

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

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

957 

958 if progress.max_value is not base.UnknownLength \ 

959 and progress.max_value > 0: 

960 percent = progress.value / progress.max_value 

961 else: 

962 percent = 0 

963 

964 num_chars = percent * width 

965 

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

967 

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

969 if marker_idx: 

970 marker += self.markers[marker_idx] 

971 

972 marker = converters.to_unicode(marker) 

973 

974 # Make sure we ignore invisible characters when filling 

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

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

977 

978 return left + marker + right 

979 

980 

981class FormatLabelBar(FormatLabel, Bar): 

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

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

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

985 Bar.__init__(self, **kwargs) 

986 

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

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

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

990 

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

992 center_len = progress.custom_len(center) 

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

994 center_right = center_left + center_len 

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

996 

997 

998class PercentageLabelBar(Percentage, FormatLabelBar): 

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

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

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

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

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

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

1005 

1006 

1007class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 

1008 '''Displays a custom variable.''' 

1009 

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

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

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

1013 self.format = format 

1014 self.width = width 

1015 self.precision = precision 

1016 VariableMixin.__init__(self, name=name) 

1017 WidgetBase.__init__(self, **kwargs) 

1018 

1019 def __call__(self, progress, data): 

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

1021 context = data.copy() 

1022 context['value'] = value 

1023 context['name'] = self.name 

1024 context['width'] = self.width 

1025 context['precision'] = self.precision 

1026 

1027 try: 

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

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

1030 value = float(value) 

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

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

1033 except (TypeError, ValueError): 

1034 if value: 

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

1036 **context) 

1037 else: 

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

1039 

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

1041 

1042 

1043class DynamicMessage(Variable): 

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

1045 pass 

1046 

1047 

1048class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 

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

1050 INTERVAL = datetime.timedelta(seconds=1) 

1051 

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

1053 microseconds=False, **kwargs): 

1054 self.microseconds = microseconds 

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

1056 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

1057 

1058 def __call__(self, progress, data): 

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

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

1061 

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

1063 

1064 def current_datetime(self): 

1065 now = datetime.datetime.now() 

1066 if not self.microseconds: 

1067 now = now.replace(microsecond=0) 

1068 

1069 return now 

1070 

1071 def current_time(self): 

1072 return self.current_datetime().time() 

1073