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 __call__(self, progress, data, format=None): 

118 '''Formats the widget into a string''' 

119 try: 

120 if self.new_style: 

121 return (format or self.format).format(**data) 

122 else: 

123 return (format or self.format) % data 

124 except (TypeError, KeyError): 

125 print('Error while formatting %r' % self.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): 

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, **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%%', **kwargs): 

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

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

634 

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

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

637 if 'percentage' in data and not data['percentage']: 

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

639 format='N/A%%') 

640 

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

642 

643 

644class SimpleProgress(FormatWidgetMixin, WidgetBase): 

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

646 

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

648 

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

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

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

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

653 

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

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

656 if data.get('max_value'): 

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

658 else: 

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

660 

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

662 if data.get('value'): 

663 data['value_s'] = data['value'] 

664 else: 

665 data['value_s'] = 0 

666 

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

668 format=format) 

669 

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

671 key = progress.min_value, progress.max_value 

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

673 if not max_width: 

674 temporary_data = data.copy() 

675 for value in key: 

676 if value is None: # pragma: no cover 

677 continue 

678 

679 temporary_data['value'] = value 

680 width = progress.custom_len(FormatWidgetMixin.__call__( 

681 self, progress, temporary_data, format=format)) 

682 if width: # pragma: no branch 

683 max_width = max(max_width or 0, width) 

684 

685 self.max_width_cache[key] = max_width 

686 

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

688 if max_width: # pragma: no branch 

689 formatted = formatted.rjust(max_width) 

690 

691 return formatted 

692 

693 

694class Bar(AutoWidthWidgetBase): 

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

696 

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

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

699 '''Creates a customizable progress bar. 

700 

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

702 

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

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

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

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

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

708 ''' 

709 

710 self.marker = create_marker(marker, marker_wrap) 

711 self.left = string_or_lambda(left) 

712 self.right = string_or_lambda(right) 

713 self.fill = string_or_lambda(fill) 

714 self.fill_left = fill_left 

715 

716 AutoWidthWidgetBase.__init__(self, **kwargs) 

717 

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

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

720 

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

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

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

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

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

726 

727 # Make sure we ignore invisible characters when filling 

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

729 

730 if self.fill_left: 

731 marker = marker.ljust(width, fill) 

732 else: 

733 marker = marker.rjust(width, fill) 

734 

735 return left + marker + right 

736 

737 

738class ReverseBar(Bar): 

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

740 

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

742 fill_left=False, **kwargs): 

743 '''Creates a customizable progress bar. 

744 

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

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

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

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

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

750 ''' 

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

752 fill_left=fill_left, **kwargs) 

753 

754 

755class BouncingBar(Bar, TimeSensitiveWidgetBase): 

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

757 

758 INTERVAL = datetime.timedelta(milliseconds=100) 

759 

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

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

762 

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

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

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

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

767 

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

769 

770 if width: # pragma: no branch 

771 value = int( 

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

773 

774 a = value % width 

775 b = width - a - 1 

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

777 a, b = b, a 

778 

779 if self.fill_left: 

780 marker = a * fill + marker + b * fill 

781 else: 

782 marker = b * fill + marker + a * fill 

783 

784 return left + marker + right 

785 

786 

787class FormatCustomText(FormatWidgetMixin, WidgetBase): 

788 mapping = {} 

789 copy = False 

790 

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

792 self.format = format 

793 self.mapping = mapping 

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

795 WidgetBase.__init__(self, **kwargs) 

796 

797 def update_mapping(self, **mapping): 

798 self.mapping.update(mapping) 

799 

800 def __call__(self, progress, data): 

801 return FormatWidgetMixin.__call__( 

802 self, progress, self.mapping, self.format) 

803 

804 

805class VariableMixin(object): 

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

807 

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

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

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

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

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

813 self.name = name 

814 

815 

816class MultiRangeBar(Bar, VariableMixin): 

817 ''' 

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

819 

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

821 

822 .. code-block:: python 

823 

824 [ 

825 ['Symbol1', amount1], 

826 ['Symbol2', amount2], 

827 ... 

828 ] 

829 ''' 

830 

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

832 VariableMixin.__init__(self, name) 

833 Bar.__init__(self, **kwargs) 

834 self.markers = [ 

835 string_or_lambda(marker) 

836 for marker in markers 

837 ] 

838 

839 def get_values(self, progress, data): 

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

841 

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

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

844 

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

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

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

848 values = self.get_values(progress, data) 

849 

850 values_sum = sum(values) 

851 if width and values_sum: 

852 middle = '' 

853 values_accumulated = 0 

854 width_accumulated = 0 

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

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

857 assert progress.custom_len(marker) == 1 

858 

859 values_accumulated += value 

860 item_width = int(values_accumulated / values_sum * width) 

861 item_width -= width_accumulated 

862 width_accumulated += item_width 

863 middle += item_width * marker 

864 else: 

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

866 assert progress.custom_len(fill) == 1 

867 middle = fill * width 

868 

869 return left + middle + right 

870 

871 

872class MultiProgressBar(MultiRangeBar): 

873 def __init__(self, 

874 name, 

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

876 # terminals don't show the characters correctly! 

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

878 **kwargs): 

879 MultiRangeBar.__init__(self, name=name, 

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

881 

882 def get_values(self, progress, data): 

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

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

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

886 # Progress is (value, max) 

887 progress_value, progress_max = progress 

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

889 

890 if progress < 0 or progress > 1: 

891 raise ValueError( 

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

893 progress) 

894 

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

896 pos = int(range_) 

897 frac = range_ % 1 

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

899 if (frac): 

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

901 

902 if self.fill_left: 

903 ranges = list(reversed(ranges)) 

904 return ranges 

905 

906 

907class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 

908 '''Displays a custom variable.''' 

909 

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

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

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

913 self.format = format 

914 self.width = width 

915 self.precision = precision 

916 VariableMixin.__init__(self, name=name) 

917 WidgetBase.__init__(self, **kwargs) 

918 

919 def __call__(self, progress, data): 

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

921 context = data.copy() 

922 context['value'] = value 

923 context['name'] = self.name 

924 context['width'] = self.width 

925 context['precision'] = self.precision 

926 

927 try: 

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

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

930 value = float(value) 

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

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

933 except (TypeError, ValueError): 

934 if value: 

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

936 **context) 

937 else: 

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

939 

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

941 

942 

943class DynamicMessage(Variable): 

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

945 pass 

946 

947 

948class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 

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

950 INTERVAL = datetime.timedelta(seconds=1) 

951 

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

953 microseconds=False, **kwargs): 

954 self.microseconds = microseconds 

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

956 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

957 

958 def __call__(self, progress, data): 

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

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

961 

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

963 

964 def current_datetime(self): 

965 now = datetime.datetime.now() 

966 if not self.microseconds: 

967 now = now.replace(microsecond=0) 

968 

969 return now 

970 

971 def current_time(self): 

972 return self.current_datetime().time() 

973