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
1074