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 >>> create_wrapper('a{}b') 

42 u'a{}b' 

43 

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

45 u'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 ''' 

193 

194 @abc.abstractmethod 

195 def __call__(self, progress, data): 

196 '''Updates the widget. 

197 

198 progress - a reference to the calling ProgressBar 

199 ''' 

200 

201 

202class AutoWidthWidgetBase(WidgetBase): 

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

204 

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

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

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

208 ''' 

209 

210 @abc.abstractmethod 

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

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

213 

214 progress - a reference to the calling ProgressBar 

215 width - The total width the widget must fill 

216 ''' 

217 

218 

219class TimeSensitiveWidgetBase(WidgetBase): 

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

221 

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

223 every `INTERVAL` 

224 ''' 

225 INTERVAL = datetime.timedelta(milliseconds=100) 

226 

227 

228class FormatLabel(FormatWidgetMixin, WidgetBase): 

229 '''Displays a formatted label 

230 

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

232 >>> class Progress(object): 

233 ... pass 

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

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

236 'test :: test ' 

237 

238 ''' 

239 

240 mapping = { 

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

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

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

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

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

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

247 'value': ('value', None), 

248 } 

249 

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

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

252 WidgetBase.__init__(self, **kwargs) 

253 

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

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

256 try: 

257 if transform is None: 

258 data[name] = data[key] 

259 else: 

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

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

262 pass 

263 

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

265 

266 

267class Timer(FormatLabel, TimeSensitiveWidgetBase): 

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

269 

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

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

272 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

273 

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

275 format_time = staticmethod(utils.format_time) 

276 

277 

278class SamplesMixin(TimeSensitiveWidgetBase): 

279 ''' 

280 Mixing for widgets that average multiple measurements 

281 

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

283 certain amount of time 

284 

285 >>> class progress: 

286 ... last_update_time = datetime.datetime.now() 

287 ... value = 1 

288 ... extra = dict() 

289 

290 >>> samples = SamplesMixin(samples=2) 

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

292 (None, None) 

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

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

295 True 

296 

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

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

299 True 

300 

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

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

303 >>> value 

304 [1, 1] 

305 

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

307 True 

308 ''' 

309 

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

311 **kwargs): 

312 self.samples = samples 

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

314 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

315 

316 def get_sample_times(self, progress, data): 

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

318 

319 def get_sample_values(self, progress, data): 

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

321 

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

323 sample_times = self.get_sample_times(progress, data) 

324 sample_values = self.get_sample_values(progress, data) 

325 

326 if sample_times: 

327 sample_time = sample_times[-1] 

328 else: 

329 sample_time = datetime.datetime.min 

330 

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

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

333 sample_times.append(progress.last_update_time) 

334 sample_values.append(progress.value) 

335 

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

337 minimum_time = progress.last_update_time - self.samples 

338 minimum_value = sample_values[-1] 

339 while (sample_times[2:] and 

340 minimum_time > sample_times[1] and 

341 minimum_value > sample_values[1]): 

342 sample_times.pop(0) 

343 sample_values.pop(0) 

344 else: 

345 if len(sample_times) > self.samples: 

346 sample_times.pop(0) 

347 sample_values.pop(0) 

348 

349 if delta: 

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

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

352 if delta_time: 

353 return delta_time, delta_value 

354 else: 

355 return None, None 

356 else: 

357 return sample_times, sample_values 

358 

359 

360class ETA(Timer): 

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

362 

363 def __init__( 

364 self, 

365 format_not_started='ETA: --:--:--', 

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

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

368 format_zero='ETA: 00:00:00', 

369 format_NA='ETA: N/A', 

370 **kwargs): 

371 

372 Timer.__init__(self, **kwargs) 

373 self.format_not_started = format_not_started 

374 self.format_finished = format_finished 

375 self.format = format 

376 self.format_zero = format_zero 

377 self.format_NA = format_NA 

378 

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

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

381 if elapsed: 

382 # The max() prevents zero division errors 

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

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

385 eta_seconds = remaining * per_item 

386 else: 

387 eta_seconds = 0 

388 

389 return eta_seconds 

390 

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

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

393 if value is None: 

394 value = data['value'] 

395 

396 if elapsed is None: 

397 elapsed = data['time_elapsed'] 

398 

399 ETA_NA = False 

400 try: 

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

402 progress, data, value=value, elapsed=elapsed) 

403 except TypeError: 

404 data['eta_seconds'] = None 

405 ETA_NA = True 

406 

407 data['eta'] = None 

408 if data['eta_seconds']: 

409 try: 

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

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

412 pass 

413 

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

415 format = self.format_not_started 

416 elif progress.end_time: 

417 format = self.format_finished 

418 elif data['eta']: 

419 format = self.format 

420 elif ETA_NA: 

421 format = self.format_NA 

422 else: 

423 format = self.format_zero 

424 

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

426 

427 

428class AbsoluteETA(ETA): 

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

430 

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

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

433 now = datetime.datetime.now() 

434 try: 

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

436 except OverflowError: # pragma: no cover 

437 return datetime.datetime.max 

438 

439 def __init__( 

440 self, 

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

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

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

444 **kwargs): 

445 ETA.__init__(self, format_not_started=format_not_started, 

446 format_finished=format_finished, format=format, **kwargs) 

447 

448 

449class AdaptiveETA(ETA, SamplesMixin): 

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

451 

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

453 Very convenient for resuming the progress halfway. 

454 ''' 

455 

456 def __init__(self, **kwargs): 

457 ETA.__init__(self, **kwargs) 

458 SamplesMixin.__init__(self, **kwargs) 

459 

460 def __call__(self, progress, data): 

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

462 delta=True) 

463 if not elapsed: 

464 value = None 

465 elapsed = 0 

466 

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

468 

469 

470class DataSize(FormatWidgetMixin, WidgetBase): 

471 ''' 

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

473 

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

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

476 ''' 

477 

478 def __init__( 

479 self, variable='value', 

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

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

482 **kwargs): 

483 self.variable = variable 

484 self.unit = unit 

485 self.prefixes = prefixes 

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

487 WidgetBase.__init__(self, **kwargs) 

488 

489 def __call__(self, progress, data): 

490 value = data[self.variable] 

491 if value is not None: 

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

493 else: 

494 scaled = power = 0 

495 

496 data['scaled'] = scaled 

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

498 data['unit'] = self.unit 

499 

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

501 

502 

503class FileTransferSpeed(FormatWidgetMixin, TimeSensitiveWidgetBase): 

504 ''' 

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

506 ''' 

507 

508 def __init__( 

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

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

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

512 **kwargs): 

513 self.unit = unit 

514 self.prefixes = prefixes 

515 self.inverse_format = inverse_format 

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

517 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

518 

519 def _speed(self, value, elapsed): 

520 speed = float(value) / elapsed 

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

522 

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

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

525 if value is None: 

526 value = data['value'] 

527 

528 elapsed = utils.deltas_to_seconds( 

529 total_seconds_elapsed, 

530 data['total_seconds_elapsed']) 

531 

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

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

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

535 else: 

536 scaled = power = 0 

537 

538 data['unit'] = self.unit 

539 if power == 0 and scaled < 0.1: 

540 if scaled > 0: 

541 scaled = 1 / scaled 

542 data['scaled'] = scaled 

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

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

545 self.inverse_format) 

546 else: 

547 data['scaled'] = scaled 

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

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

550 

551 

552class AdaptiveTransferSpeed(FileTransferSpeed, SamplesMixin): 

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

554 ''' 

555 

556 def __init__(self, **kwargs): 

557 FileTransferSpeed.__init__(self, **kwargs) 

558 SamplesMixin.__init__(self, **kwargs) 

559 

560 def __call__(self, progress, data): 

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

562 delta=True) 

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

564 

565 

566class AnimatedMarker(TimeSensitiveWidgetBase): 

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

568 it were rotating. 

569 ''' 

570 

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

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

573 self.markers = markers 

574 self.marker_wrap = create_wrapper(marker_wrap) 

575 self.default = default or markers[0] 

576 self.fill_wrap = create_wrapper(fill_wrap) 

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

578 WidgetBase.__init__(self, **kwargs) 

579 

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

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

582 finished''' 

583 

584 if progress.end_time: 

585 return self.default 

586 

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

588 if self.marker_wrap: 

589 marker = self.marker_wrap.format(marker) 

590 

591 if self.fill: 

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

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

594 marker)) 

595 else: 

596 fill = '' 

597 

598 # Python 3 returns an int when indexing bytes 

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

600 marker = bytes(marker) 

601 fill = fill.encode() 

602 else: 

603 # cast fill to the same type as marker 

604 fill = type(marker)(fill) 

605 

606 return fill + marker 

607 

608 

609# Alias for backwards compatibility 

610RotatingMarker = AnimatedMarker 

611 

612 

613class Counter(FormatWidgetMixin, WidgetBase): 

614 '''Displays the current count''' 

615 

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

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

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

619 

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

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

622 

623 

624class Percentage(FormatWidgetMixin, WidgetBase): 

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

626 

627 def __init__(self, format='%(percentage)3d%%', **kwargs): 

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

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

630 

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

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

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

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

635 format='N/A%%') 

636 

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

638 

639 

640class SimpleProgress(FormatWidgetMixin, WidgetBase): 

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

642 

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

644 

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

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

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

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

649 

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

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

652 if data.get('max_value'): 

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

654 else: 

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

656 

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

658 if data.get('value'): 

659 data['value_s'] = data['value'] 

660 else: 

661 data['value_s'] = 0 

662 

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

664 format=format) 

665 

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

667 key = progress.min_value, progress.max_value 

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

669 if not max_width: 

670 temporary_data = data.copy() 

671 for value in key: 

672 if value is None: # pragma: no cover 

673 continue 

674 

675 temporary_data['value'] = value 

676 width = progress.custom_len(FormatWidgetMixin.__call__( 

677 self, progress, temporary_data, format=format)) 

678 if width: # pragma: no branch 

679 max_width = max(max_width or 0, width) 

680 

681 self.max_width_cache[key] = max_width 

682 

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

684 if max_width: # pragma: no branch 

685 formatted = formatted.rjust(max_width) 

686 

687 return formatted 

688 

689 

690class Bar(AutoWidthWidgetBase): 

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

692 

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

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

695 '''Creates a customizable progress bar. 

696 

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

698 

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

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

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

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

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

704 ''' 

705 

706 self.marker = create_marker(marker, marker_wrap) 

707 self.left = string_or_lambda(left) 

708 self.right = string_or_lambda(right) 

709 self.fill = string_or_lambda(fill) 

710 self.fill_left = fill_left 

711 

712 AutoWidthWidgetBase.__init__(self, **kwargs) 

713 

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

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

716 

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

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

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

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

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

722 

723 # Make sure we ignore invisible characters when filling 

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

725 

726 if self.fill_left: 

727 marker = marker.ljust(width, fill) 

728 else: 

729 marker = marker.rjust(width, fill) 

730 

731 return left + marker + right 

732 

733 

734class ReverseBar(Bar): 

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

736 

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

738 fill_left=False, **kwargs): 

739 '''Creates a customizable progress bar. 

740 

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

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

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

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

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

746 ''' 

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

748 fill_left=fill_left, **kwargs) 

749 

750 

751class BouncingBar(Bar, TimeSensitiveWidgetBase): 

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

753 

754 INTERVAL = datetime.timedelta(milliseconds=100) 

755 

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

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

758 

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

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

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

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

763 

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

765 

766 if width: # pragma: no branch 

767 value = int( 

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

769 

770 a = value % width 

771 b = width - a - 1 

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

773 a, b = b, a 

774 

775 if self.fill_left: 

776 marker = a * fill + marker + b * fill 

777 else: 

778 marker = b * fill + marker + a * fill 

779 

780 return left + marker + right 

781 

782 

783class FormatCustomText(FormatWidgetMixin, WidgetBase): 

784 mapping = {} 

785 

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

787 self.format = format 

788 self.mapping = mapping 

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

790 WidgetBase.__init__(self, **kwargs) 

791 

792 def update_mapping(self, **mapping): 

793 self.mapping.update(mapping) 

794 

795 def __call__(self, progress, data): 

796 return FormatWidgetMixin.__call__( 

797 self, progress, self.mapping, self.format) 

798 

799 

800class VariableMixin(object): 

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

802 

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

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

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

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

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

808 self.name = name 

809 

810 

811class MultiRangeBar(Bar, VariableMixin): 

812 ''' 

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

814 

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

816 

817 .. code-block:: python 

818 

819 [ 

820 ['Symbol1', amount1], 

821 ['Symbol2', amount2], 

822 ... 

823 ] 

824 ''' 

825 

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

827 VariableMixin.__init__(self, name) 

828 Bar.__init__(self, **kwargs) 

829 self.markers = [ 

830 string_or_lambda(marker) 

831 for marker in markers 

832 ] 

833 

834 def get_values(self, progress, data): 

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

836 

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

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

839 

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

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

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

843 values = self.get_values(progress, data) 

844 

845 values_sum = sum(values) 

846 if width and values_sum: 

847 middle = '' 

848 values_accumulated = 0 

849 width_accumulated = 0 

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

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

852 assert progress.custom_len(marker) == 1 

853 

854 values_accumulated += value 

855 item_width = int(values_accumulated / values_sum * width) 

856 item_width -= width_accumulated 

857 width_accumulated += item_width 

858 middle += item_width * marker 

859 else: 

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

861 assert progress.custom_len(fill) == 1 

862 middle = fill * width 

863 

864 return left + middle + right 

865 

866 

867class MultiProgressBar(MultiRangeBar): 

868 def __init__(self, 

869 name, 

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

871 # terminals don't show the characters correctly! 

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

873 **kwargs): 

874 MultiRangeBar.__init__(self, name=name, 

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

876 

877 def get_values(self, progress, data): 

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

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

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

881 # Progress is (value, max) 

882 progress_value, progress_max = progress 

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

884 

885 if progress < 0 or progress > 1: 

886 raise ValueError( 

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

888 progress) 

889 

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

891 pos = int(range_) 

892 frac = range_ % 1 

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

894 if (frac): 

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

896 

897 if self.fill_left: 

898 ranges = list(reversed(ranges)) 

899 return ranges 

900 

901 

902class Variable(FormatWidgetMixin, VariableMixin, WidgetBase): 

903 '''Displays a custom variable.''' 

904 

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

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

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

908 self.format = format 

909 self.width = width 

910 self.precision = precision 

911 VariableMixin.__init__(self, name=name) 

912 WidgetBase.__init__(self, **kwargs) 

913 

914 def __call__(self, progress, data): 

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

916 context = data.copy() 

917 context['value'] = value 

918 context['name'] = self.name 

919 context['width'] = self.width 

920 context['precision'] = self.precision 

921 

922 try: 

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

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

925 value = float(value) 

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

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

928 except (TypeError, ValueError): 

929 if value: 

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

931 **context) 

932 else: 

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

934 

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

936 

937 

938class DynamicMessage(Variable): 

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

940 pass 

941 

942 

943class CurrentTime(FormatWidgetMixin, TimeSensitiveWidgetBase): 

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

945 INTERVAL = datetime.timedelta(seconds=1) 

946 

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

948 microseconds=False, **kwargs): 

949 self.microseconds = microseconds 

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

951 TimeSensitiveWidgetBase.__init__(self, **kwargs) 

952 

953 def __call__(self, progress, data): 

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

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

956 

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

958 

959 def current_datetime(self): 

960 now = datetime.datetime.now() 

961 if not self.microseconds: 

962 now = now.replace(microsecond=0) 

963 

964 return now 

965 

966 def current_time(self): 

967 return self.current_datetime().time() 

968