diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..44ccfb21 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,42 @@ +name: "CodeQL" + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + schedule: + - cron: "24 21 * * 1" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python, javascript ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + if: ${{ matrix.language == 'python' || matrix.language == 'javascript' }} + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/README.rst b/README.rst index 94b33333..434b5756 100644 --- a/README.rst +++ b/README.rst @@ -72,13 +72,19 @@ The progressbar module is very easy to use, yet very powerful. It will also automatically enable features like auto-resizing when the system supports it. ****************************************************************************** -Known issues +Security contact information ****************************************************************************** -Due to limitations in both the IDLE shell and the Jetbrains (Pycharm) shells this progressbar cannot function properly within those. +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. + +****************************************************************************** +Known issues +****************************************************************************** +- The Jetbrains (PyCharm, etc) editors work out of the box, but for more advanced features such as the `MultiBar` support you will need to enable the "Enable terminal in output console" checkbox in the Run dialog. - The IDLE editor doesn't support these types of progress bars at all: https://bugs.python.org/issue23220 -- The Jetbrains (Pycharm) editors partially work but break with fast output. As a workaround make sure you only write to either `sys.stdout` (regular print) or `sys.stderr` at the same time. If you do plan to use both, make sure you wait about ~200 milliseconds for the next output or it will break regularly. Linked issue: https://github.com/WoLpH/python-progressbar/issues/115 - Jupyter notebooks buffer `sys.stdout` which can cause mixed output. This issue can be resolved easily using: `import sys; sys.stdout.flush()`. Linked issue: https://github.com/WoLpH/python-progressbar/issues/173 ****************************************************************************** @@ -152,6 +158,38 @@ In most cases the following will work as well, as long as you initialize the logging.error('Got %d', i) time.sleep(0.2) +Multiple (threaded) progressbars +============================================================================== + +.. code:: python + + import random + import threading + import time + + import progressbar + + BARS = 5 + N = 50 + + + def do_something(bar): + for i in bar(range(N)): + # Sleep up to 0.1 seconds + time.sleep(random.random() * 0.1) + + # print messages at random intervals to show how extra output works + if random.random() > 0.9: + bar.print('random message for bar', bar, i) + + + with progressbar.MultiBar() as multibar: + for i in range(BARS): + # Get a progressbar + bar = multibar[f'Thread label here {i}'] + # Create a thread and pass the progressbar + threading.Thread(target=do_something, args=(bar,)).start() + Context wrapper ============================================================================== .. code:: python @@ -238,55 +276,72 @@ Bar with wide Chinese (or other multibyte) characters for i in bar(range(10)): time.sleep(0.1) -Showing multiple (threaded) independent progress bars in parallel +Showing multiple independent progress bars in parallel ============================================================================== -While this method works fine and will continue to work fine, a smarter and -fully automatic version of this is currently being made: -https://github.com/WoLpH/python-progressbar/issues/176 - .. code:: python import random import sys - import threading import time import progressbar - output_lock = threading.Lock() + BARS = 5 + N = 100 + + # Construct the list of progress bars with the `line_offset` so they draw + # below each other + bars = [] + for i in range(BARS): + bars.append( + progressbar.ProgressBar( + max_value=N, + # We add 1 to the line offset to account for the `print_fd` + line_offset=i + 1, + max_error=False, + ) + ) + # Create a file descriptor for regular printing as well + print_fd = progressbar.LineOffsetStreamWrapper(sys.stdout, 0) - class LineOffsetStreamWrapper: - UP = '\033[F' - DOWN = '\033[B' + # The progress bar updates, normally you would do something useful here + for i in range(N * BARS): + time.sleep(0.005) - def __init__(self, lines=0, stream=sys.stderr): - self.stream = stream - self.lines = lines + # Increment one of the progress bars at random + bars[random.randrange(0, BARS)].increment() - def write(self, data): - with output_lock: - self.stream.write(self.UP * self.lines) - self.stream.write(data) - self.stream.write(self.DOWN * self.lines) - self.stream.flush() + # Print a status message to the `print_fd` below the progress bars + print(f'Hi, we are at update {i+1} of {N * BARS}', file=print_fd) - def __getattr__(self, name): - return getattr(self.stream, name) + # Cleanup the bars + for bar in bars: + bar.finish() + # Add a newline to make sure the next print starts on a new line + print() - bars = [] - for i in range(5): - bars.append( - progressbar.ProgressBar( - fd=LineOffsetStreamWrapper(i), - max_value=1000, - ) - ) +****************************************************************************** - if i: - print('Reserve a line for the progressbar') +Naturally we can do this from separate threads as well: + +.. code:: python + + import random + import threading + import time + + import progressbar + + BARS = 5 + N = 100 + + # Create the bars with the given line offset + bars = [] + for line_offset in range(BARS): + bars.append(progressbar.ProgressBar(line_offset=line_offset, max_value=N)) class Worker(threading.Thread): @@ -295,10 +350,12 @@ https://github.com/WoLpH/python-progressbar/issues/176 self.bar = bar def run(self): - for i in range(1000): - time.sleep(random.random() / 100) + for i in range(N): + time.sleep(random.random() / 25) self.bar.update(i) for bar in bars: Worker(bar).start() + + print() diff --git a/docs/conf.py b/docs/conf.py index 15d5ba34..df9ef70e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,10 +61,7 @@ # General information about the project. project = u'Progress Bar' project_slug = ''.join(project.capitalize().split()) -copyright = u'%s, %s' % ( - datetime.date.today().year, - metadata.__author__, -) +copyright = f'{datetime.date.today().year}, {metadata.__author__}' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -190,7 +187,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project_slug +htmlhelp_basename = f'{project_slug}doc' # -- Options for LaTeX output -------------------------------------------- @@ -209,8 +206,13 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', '%s.tex' % project_slug, u'%s Documentation' % project, - metadata.__author__, 'manual'), + ( + 'index', + f'{project_slug}.tex', + f'{project} Documentation', + metadata.__author__, + 'manual', + ) ] # The name of an image file (relative to this directory) to place at the top of @@ -239,8 +241,13 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', project_slug.lower(), u'%s Documentation' % project, - [metadata.__author__], 1) + ( + 'index', + project_slug.lower(), + f'{project} Documentation', + [metadata.__author__], + 1, + ) ] # If true, show URL addresses after external links. @@ -253,9 +260,15 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', project_slug, u'%s Documentation' % project, - metadata.__author__, project_slug, 'One line description of project.', - 'Miscellaneous'), + ( + 'index', + project_slug, + f'{project} Documentation', + metadata.__author__, + project_slug, + 'One line description of project.', + 'Miscellaneous', + ) ] # Documents to append as an appendix to all manuals. diff --git a/examples.py b/examples.py index 1c033839..7fb48efe 100644 --- a/examples.py +++ b/examples.py @@ -39,19 +39,19 @@ def fast_example(): @example def shortcut_example(): - for i in progressbar.progressbar(range(10)): + for _ in progressbar.progressbar(range(10)): time.sleep(0.1) @example def prefixed_shortcut_example(): - for i in progressbar.progressbar(range(10), prefix='Hi: '): + for _ in progressbar.progressbar(range(10), prefix='Hi: '): time.sleep(0.1) @example def templated_shortcut_example(): - for i in progressbar.progressbar(range(10), suffix='{seconds_elapsed:.1}'): + for _ in progressbar.progressbar(range(10), suffix='{seconds_elapsed:.1}'): time.sleep(0.1) @@ -142,18 +142,14 @@ def multi_range_bar_example(): @example def multi_progress_bar_example(left=True): - jobs = [ - # Each job takes between 1 and 10 steps to complete - [0, random.randint(1, 10)] - for i in range(25) # 25 jobs total - ] + jobs = [[0, random.randint(1, 10)] for _ in range(25)] widgets = [ progressbar.Percentage(), ' ', progressbar.MultiProgressBar('jobs', fill_left=left), ] - max_value = sum([total for progress, total in jobs]) + max_value = sum(total for progress, total in jobs) with progressbar.ProgressBar(widgets=widgets, max_value=max_value) as bar: while True: incomplete_jobs = [ @@ -165,7 +161,7 @@ def multi_progress_bar_example(left=True): break which = random.choice(incomplete_jobs) jobs[which][0] += 1 - progress = sum([progress for progress, total in jobs]) + progress = sum(progress for progress, total in jobs) bar.update(progress, jobs=jobs, force=True) time.sleep(0.02) @@ -181,10 +177,10 @@ def granular_progress_example(): progressbar.GranularBar(markers=" ⡀⡄⡆⡇⣇⣧⣷⣿", left='', right='|'), progressbar.GranularBar(markers=" .oO", left='', right=''), ] - for i in progressbar.progressbar(list(range(100)), widgets=widgets): + for _ in progressbar.progressbar(list(range(100)), widgets=widgets): time.sleep(0.03) - for i in progressbar.progressbar(iter(range(100)), widgets=widgets): + for _ in progressbar.progressbar(iter(range(100)), widgets=widgets): time.sleep(0.03) @@ -216,17 +212,20 @@ def file_transfer_example(): @example def custom_file_transfer_example(): + + + class CrazyFileTransferSpeed(progressbar.FileTransferSpeed): ''' It's bigger between 45 and 80 percent ''' def update(self, bar): if 45 < bar.percentage() < 80: - return 'Bigger Now ' + progressbar.FileTransferSpeed.update( - self, bar) + return f'Bigger Now {progressbar.FileTransferSpeed.update(self, bar)}' else: return progressbar.FileTransferSpeed.update(self, bar) + widgets = [ CrazyFileTransferSpeed(), ' <<<', progressbar.Bar(), '>>> ', @@ -300,7 +299,7 @@ def basic_progress(): def progress_with_automatic_max(): # Progressbar can guess max_value automatically. bar = progressbar.ProgressBar() - for i in bar(range(8)): + for _ in bar(range(8)): time.sleep(0.1) @@ -308,7 +307,7 @@ def progress_with_automatic_max(): def progress_with_unavailable_max(): # Progressbar can't guess max_value. bar = progressbar.ProgressBar(max_value=8) - for i in bar((i for i in range(8))): + for _ in bar(iter(range(8))): time.sleep(0.1) @@ -316,7 +315,7 @@ def progress_with_unavailable_max(): def animated_marker(): bar = progressbar.ProgressBar( widgets=['Working: ', progressbar.AnimatedMarker()]) - for i in bar((i for i in range(5))): + for _ in bar(iter(range(5))): time.sleep(0.1) @@ -327,7 +326,7 @@ def filling_bar_animated_marker(): marker=progressbar.AnimatedMarker(fill='#'), ), ]) - for i in bar(range(15)): + for _ in bar(range(15)): time.sleep(0.1) @@ -336,7 +335,7 @@ def counter_and_timer(): widgets = ['Processed: ', progressbar.Counter('Counter: %(value)05d'), ' lines (', progressbar.Timer(), ')'] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(15))): + for _ in bar(iter(range(15))): time.sleep(0.1) @@ -345,7 +344,7 @@ def format_label(): widgets = [progressbar.FormatLabel( 'Processed: %(value)d lines (in: %(elapsed)s)')] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(15))): + for _ in bar(iter(range(15))): time.sleep(0.1) @@ -353,7 +352,7 @@ def format_label(): def animated_balloons(): widgets = ['Balloon: ', progressbar.AnimatedMarker(markers='.oO@* ')] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(24))): + for _ in bar(iter(range(24))): time.sleep(0.1) @@ -363,7 +362,7 @@ def animated_arrows(): try: widgets = ['Arrows: ', progressbar.AnimatedMarker(markers='←↖↑↗→↘↓↙')] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(24))): + for _ in bar(iter(range(24))): time.sleep(0.1) except UnicodeError: sys.stdout.write('Unicode error: skipping example') @@ -375,7 +374,7 @@ def animated_filled_arrows(): try: widgets = ['Arrows: ', progressbar.AnimatedMarker(markers='◢◣◤◥')] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(24))): + for _ in bar(iter(range(24))): time.sleep(0.1) except UnicodeError: sys.stdout.write('Unicode error: skipping example') @@ -387,7 +386,7 @@ def animated_wheels(): try: widgets = ['Wheels: ', progressbar.AnimatedMarker(markers='◐◓◑◒')] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(24))): + for _ in bar(iter(range(24))): time.sleep(0.1) except UnicodeError: sys.stdout.write('Unicode error: skipping example') @@ -400,7 +399,7 @@ def format_label_bouncer(): progressbar.BouncingBar(), ] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(100))): + for _ in bar(iter(range(100))): time.sleep(0.01) @@ -410,7 +409,7 @@ def format_label_rotating_bouncer(): progressbar.BouncingBar(marker=progressbar.RotatingMarker())] bar = progressbar.ProgressBar(widgets=widgets) - for i in bar((i for i in range(18))): + for _ in bar(iter(range(18))): time.sleep(0.1) @@ -488,7 +487,7 @@ def incrementing_bar(): progressbar.Percentage(), progressbar.Bar(), ], max_value=10).start() - for i in range(10): + for _ in range(10): # do something time.sleep(0.1) bar += 1 @@ -545,7 +544,7 @@ def adaptive_eta_without_value_change(): progressbar.AdaptiveTransferSpeed(), ], max_value=2, poll_interval=0.0001) bar.start() - for i in range(100): + for _ in range(100): bar.update(1) time.sleep(0.1) bar.finish() @@ -556,7 +555,7 @@ def iterator_with_max_value(): # Testing using progressbar as an iterator with a max value bar = progressbar.ProgressBar() - for n in bar(iter(range(100)), 100): + for _ in bar(iter(range(100)), 100): # iter range is a way to get an iterator in both python 2 and 3 pass @@ -622,12 +621,12 @@ def user_variables(): num_subtasks = sum(len(x) for x in tasks.values()) with progressbar.ProgressBar( - prefix='{variables.task} >> {variables.subtask}', - variables={'task': '--', 'subtask': '--'}, - max_value=10 * num_subtasks) as bar: + prefix='{variables.task} >> {variables.subtask}', + variables={'task': '--', 'subtask': '--'}, + max_value=10 * num_subtasks) as bar: for tasks_name, subtasks in tasks.items(): for subtask_name in subtasks: - for i in range(10): + for _ in range(10): bar.update(bar.value + 1, task=tasks_name, subtask=subtask_name) time.sleep(0.1) @@ -656,7 +655,7 @@ def format_custom_text(): @example def simple_api_example(): bar = progressbar.ProgressBar(widget_kwargs=dict(fill='█')) - for i in bar(range(200)): + for _ in bar(range(200)): time.sleep(0.02) diff --git a/progressbar/__init__.py b/progressbar/__init__.py index 33d7c719..55525543 100644 --- a/progressbar/__init__.py +++ b/progressbar/__init__.py @@ -35,6 +35,8 @@ from .widgets import Timer from .widgets import Variable from .widgets import VariableMixin +from .terminal.stream import LineOffsetStreamWrapper +from .multi import SortKey, MultiBar __date__ = str(date.today()) __all__ = [ @@ -73,4 +75,7 @@ 'NullBar', '__author__', '__version__', + 'LineOffsetStreamWrapper', + 'MultiBar', + 'SortKey', ] diff --git a/progressbar/bar.py b/progressbar/bar.py index 806a98a8..01831fd6 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -1,7 +1,9 @@ from __future__ import annotations import abc +import itertools import logging +import math import os import sys import time @@ -11,11 +13,12 @@ from datetime import datetime from typing import Type -import math from python_utils import converters, types +import progressbar.terminal.stream from . import ( base, + terminal, utils, widgets, widgets as widgets_module, # Avoid name collision @@ -37,7 +40,7 @@ class ProgressBarMixinBase(abc.ABC): #: fall back to 80 if auto detection is not possible. term_width: int = 80 #: The widgets to render, defaults to the result of `default_widget()` - widgets: types.List[widgets_module.WidgetBase] + widgets: types.MutableSequence[widgets_module.WidgetBase | str] #: When going beyond the max_value, raise an error if True or silently #: ignore otherwise max_error: bool @@ -90,10 +93,7 @@ def get_last_update_time(self) -> types.Optional[datetime]: return None def set_last_update_time(self, value: types.Optional[datetime]): - if value: - self._last_update_time = time.mktime(value.timetuple()) - else: - self._last_update_time = None + self._last_update_time = time.mktime(value.timetuple()) if value else None last_update_time = property(get_last_update_time, set_last_update_time) @@ -111,24 +111,33 @@ def finish(self): # pragma: no cover def __del__(self): if not self._finished and self._started: # pragma: no cover - try: - self.finish() - except Exception: - # Never raise during cleanup. We're too late now - logging.debug( - 'Exception raised during ProgressBar cleanup', - exc_info=True, - ) + self.finish() def __getstate__(self): return self.__dict__ - def data(self) -> types.Dict[str, types.Any]: + def data(self) -> types.Dict[str, types.Any]: # pragma: no cover raise NotImplementedError() + def started(self) -> bool: + return self._finished or self._started + + def finished(self) -> bool: + return self._finished + class ProgressBarBase(types.Iterable, ProgressBarMixinBase): - pass + _index_counter = itertools.count() + index: int = -1 + label: str = '' + + def __init__(self, **kwargs): + self.index = next(self._index_counter) + super().__init__(**kwargs) + + def __repr__(self): + label = f': {self.label}' if self.label else '' + return f'<{self.__class__.__name__}#{self.index}{label}>' class DefaultFdMixin(ProgressBarMixinBase): @@ -141,15 +150,25 @@ class DefaultFdMixin(ProgressBarMixinBase): #: Whether to print line breaks. This is useful for logging the #: progressbar. When disabled the current line is overwritten. line_breaks: bool = True - #: Enable or disable colors. Defaults to auto detection - enable_colors: bool = False + #: Specify the type and number of colors to support. Defaults to auto + #: detection based on the file descriptor type (i.e. interactive terminal) + #: environment variables such as `COLORTERM` and `TERM`. Color output can + #: be forced in non-interactive terminals using the + #: `PROGRESSBAR_ENABLE_COLORS` environment variable which can also be used + #: to force a specific number of colors by specifying `24bit`, `256` or + #: `16`. + #: For true (24 bit/16M) color support you can use `COLORTERM=truecolor`. + #: For 256 color support you can use `TERM=xterm-256color`. + #: For 16 colorsupport you can use `TERM=xterm`. + enable_colors: terminal.ColorSupport | bool | None = terminal.color_support def __init__( self, fd: base.IO = sys.stderr, is_terminal: bool | None = None, line_breaks: bool | None = None, - enable_colors: bool | None = None, + enable_colors: terminal.ColorSupport | None = None, + line_offset: int = 0, **kwargs, ): if fd is sys.stdout: @@ -158,6 +177,11 @@ def __init__( elif fd is sys.stderr: fd = utils.streams.original_stderr + if line_offset: + fd = progressbar.terminal.stream.LineOffsetStreamWrapper( + line_offset, fd + ) + self.fd = fd self.is_ansi_terminal = utils.is_ansi_terminal(fd) @@ -177,14 +201,34 @@ def __init__( # Check if ANSI escape characters are enabled (suitable for iteractive # terminals), or should be stripped off (suitable for log files) if enable_colors is None: - enable_colors = utils.env_flag( - 'PROGRESSBAR_ENABLE_COLORS', self.is_ansi_terminal + colors = ( + utils.env_flag('PROGRESSBAR_ENABLE_COLORS'), + utils.env_flag('FORCE_COLOR'), + self.is_ansi_terminal, ) - self.enable_colors = bool(enable_colors) + for color_enabled in colors: # pragma: no branch + if color_enabled is not None: + if color_enabled: + enable_colors = terminal.color_support + else: + enable_colors = terminal.ColorSupport.NONE + break + + elif enable_colors is True: + enable_colors = terminal.ColorSupport.XTERM_256 + elif enable_colors is False: + enable_colors = terminal.ColorSupport.NONE + elif not isinstance(enable_colors, terminal.ColorSupport): + raise ValueError(f'Invalid color support value: {enable_colors}') + + self.enable_colors = enable_colors ProgressBarMixinBase.__init__(self, **kwargs) + def print(self, *args, **kwargs): + print(*args, file=self.fd, **kwargs) + def update(self, *args, **kwargs): ProgressBarMixinBase.update(self, *args, **kwargs) @@ -192,11 +236,7 @@ def update(self, *args, **kwargs): if not self.enable_colors: line = utils.no_color(line) - if self.line_breaks: - line = line.rstrip() + '\n' - else: - line = '\r' + line - + line = line.rstrip() + '\n' if self.line_breaks else '\r' + line try: # pragma: no cover self.fd.write(line) except UnicodeEncodeError: # pragma: no cover @@ -392,6 +432,9 @@ class ProgressBar( from a label using `format='{variables.my_var}'`. These values can be updated using `bar.update(my_var='newValue')` This can also be used to set initial values for variables' widgets + line_offset (int): The number of lines to offset the progressbar from + your current line. This is useful if you have other output or + multiple progressbars A common way of using it is like: @@ -434,12 +477,15 @@ class ProgressBar( # update every 50 milliseconds (up to a 20 times per second) _MINIMUM_UPDATE_INTERVAL: float = 0.050 _last_update_time: types.Optional[float] = None + paused: bool = False def __init__( self, min_value: T = 0, max_value: T | types.Type[base.UnknownLength] | None = None, - widgets: types.Optional[types.List[widgets_module.WidgetBase]] = None, + widgets: types.Optional[ + types.Sequence[widgets_module.WidgetBase | str] + ] = None, left_justify: bool = True, initial_value: T = 0, poll_interval: types.Optional[float] = None, @@ -474,13 +520,10 @@ def __init__( ) poll_interval = kwargs.get('poll') - if max_value: - # mypy doesn't understand that a boolean check excludes - # `UnknownLength` - if min_value > max_value: # type: ignore - raise ValueError( - 'Max value needs to be bigger than the min ' 'value' - ) + if max_value and min_value > max_value: + raise ValueError( + 'Max value needs to be bigger than the min ' 'value' + ) self.min_value = min_value # Legacy issue, `max_value` can be `None` before execution. After # that it either has a value or is `UnknownLength` @@ -533,9 +576,11 @@ def __init__( # A dictionary of names that can be used by Variable and FormatWidget self.variables = utils.AttributeDict(variables or {}) for widget in self.widgets: - if isinstance(widget, widgets_module.VariableMixin): - if widget.name not in self.variables: - self.variables[widget.name] = None + if ( + isinstance(widget, widgets_module.VariableMixin) + and widget.name not in self.variables + ): + self.variables[widget.name] = None @property def dynamic_messages(self): # pragma: no cover @@ -555,11 +600,11 @@ def init(self): self.start_time = None self.updates = 0 self.end_time = None - self.extra = dict() + self.extra = {} self._last_update_timer = timeit.default_timer() @property - def percentage(self): + def percentage(self) -> float | None: '''Return current percentage, returns None if no max_value is given >>> progress = ProgressBar() @@ -678,7 +723,7 @@ def default_widgets(self): widgets.Percentage(**self.widget_kwargs), ' ', widgets.SimpleProgress( - format='(%s)' % widgets.SimpleProgress.DEFAULT_FORMAT, + format=f'({widgets.SimpleProgress.DEFAULT_FORMAT})', **self.widget_kwargs, ), ' ', @@ -717,11 +762,7 @@ def __iter__(self): def __next__(self): try: - if self._iterable is None: # pragma: no cover - value = self.value - else: - value = next(self._iterable) - + value = self.value if self._iterable is None else next(self._iterable) if self.start_time is None: self.start() else: @@ -755,6 +796,8 @@ def increment(self, value=1, *args, **kwargs): def _needs_update(self): 'Returns whether the ProgressBar should redraw the line.' + if self.paused: + return False delta = timeit.default_timer() - self._last_update_timer if delta < self.min_poll_interval: # Prevent updating too often @@ -785,31 +828,36 @@ def update(self, value=None, force=False, **kwargs): self.start() return self.update(value, force=force, **kwargs) - if value is not None and value is not base.UnknownLength: + if ( + value is not None + and value is not base.UnknownLength + and isinstance(value, int) + ): if self.max_value is base.UnknownLength: # Can't compare against unknown lengths so just update pass - elif self.min_value <= value <= self.max_value: # pragma: no cover - # Correct value, let's accept - pass - elif self.max_error: + elif self.min_value > value: # type: ignore raise ValueError( - 'Value %s is out of range, should be between %s and %s' - % (value, self.min_value, self.max_value) + f'Value {value} is too small. Should be between {self.min_value} and {self.max_value}' ) - else: - self.max_value = value + elif self.max_value < value: # type: ignore + if self.max_error: + raise ValueError( + f'Value {value} is too large. Should be between {self.min_value} and {self.max_value}' + ) + else: + value = self.max_value self.previous_value = self.value - self.value = value + self.value = value # type: ignore # Save the updated values for dynamic messages variables_changed = False for key in kwargs: if key not in self.variables: raise TypeError( - 'update() got an unexpected keyword ' - + 'argument {0!r}'.format(key) + 'update() got an unexpected variable name as argument ' + '{0!r}'.format(key) ) elif self.variables[key] != kwargs[key]: self.variables[key] = kwargs[key] @@ -819,7 +867,7 @@ def update(self, value=None, force=False, **kwargs): self.updates += 1 ResizableMixin.update(self, value=value) ProgressBarBase.update(self, value=value) - StdRedirectMixin.update(self, value=value) + StdRedirectMixin.update(self, value=value) # type: ignore # Only flush if something was actually written self.fd.flush() diff --git a/progressbar/base.py b/progressbar/base.py index 8639f557..8e007914 100644 --- a/progressbar/base.py +++ b/progressbar/base.py @@ -23,8 +23,8 @@ class Undefined(metaclass=FalseMeta): try: # pragma: no cover IO = types.IO # type: ignore TextIO = types.TextIO # type: ignore -except AttributeError: +except AttributeError: # pragma: no cover from typing.io import IO, TextIO # type: ignore -assert IO -assert TextIO +assert IO is not None +assert TextIO is not None diff --git a/progressbar/multi.py b/progressbar/multi.py new file mode 100644 index 00000000..087083dc --- /dev/null +++ b/progressbar/multi.py @@ -0,0 +1,339 @@ +from __future__ import annotations + +import enum +import io +import itertools +import operator +import sys +import threading +import time +import timeit +import typing +from datetime import timedelta + +import python_utils + +from . import bar, terminal +from .terminal import stream + +SortKeyFunc = typing.Callable[[bar.ProgressBar], typing.Any] + + +class SortKey(str, enum.Enum): + ''' + Sort keys for the MultiBar + + This is a string enum, so you can use any + progressbar attribute or property as a sort key. + + Note that the multibar defaults to lazily rendering only the changed + progressbars. This means that sorting by dynamic attributes such as + `value` might result in more rendering which can have a small performance + impact. + ''' + + CREATED = 'index' + LABEL = 'label' + VALUE = 'value' + PERCENTAGE = 'percentage' + + +class MultiBar(typing.Dict[str, bar.ProgressBar]): + fd: typing.TextIO + _buffer: io.StringIO + + #: The format for the label to append/prepend to the progressbar + label_format: str + #: Automatically prepend the label to the progressbars + prepend_label: bool + #: Automatically append the label to the progressbars + append_label: bool + #: If `initial_format` is `None`, the progressbar rendering is used + # which will *start* the progressbar. That means the progressbar will + # have no knowledge of your data and will run as an infinite progressbar. + initial_format: str | None + #: If `finished_format` is `None`, the progressbar rendering is used. + finished_format: str | None + + #: The multibar updates at a fixed interval regardless of the progressbar + # updates + update_interval: float + remove_finished: float | None + + #: The kwargs passed to the progressbar constructor + progressbar_kwargs: typing.Dict[str, typing.Any] + + #: The progressbar sorting key function + sort_keyfunc: SortKeyFunc + + _previous_output: list[str] + _finished_at: dict[bar.ProgressBar, float] + _labeled: set[bar.ProgressBar] + _print_lock: threading.RLock = threading.RLock() + _thread: threading.Thread | None = None + _thread_finished: threading.Event = threading.Event() + _thread_closed: threading.Event = threading.Event() + + def __init__( + self, + bars: typing.Iterable[tuple[str, bar.ProgressBar]] | None = None, + fd=sys.stderr, + prepend_label: bool = True, + append_label: bool = False, + label_format='{label:20.20} ', + initial_format: str | None = '{label:20.20} Not yet started', + finished_format: str | None = None, + update_interval: float = 1 / 60.0, # 60fps + show_initial: bool = True, + show_finished: bool = True, + remove_finished: timedelta | float = timedelta(seconds=3600), + sort_key: str | SortKey = SortKey.CREATED, + sort_reverse: bool = True, + sort_keyfunc: SortKeyFunc | None = None, + **progressbar_kwargs, + ): + self.fd = fd + + self.prepend_label = prepend_label + self.append_label = append_label + self.label_format = label_format + self.initial_format = initial_format + self.finished_format = finished_format + + self.update_interval = update_interval + + self.show_initial = show_initial + self.show_finished = show_finished + self.remove_finished = python_utils.delta_to_seconds_or_none( + remove_finished + ) + + self.progressbar_kwargs = progressbar_kwargs + + if sort_keyfunc is None: + sort_keyfunc = operator.attrgetter(sort_key) + + self.sort_keyfunc = sort_keyfunc + self.sort_reverse = sort_reverse + + self._labeled = set() + self._finished_at = {} + self._previous_output = [] + self._buffer = io.StringIO() + + super().__init__(bars or {}) + + def __setitem__(self, key: str, value: bar.ProgressBar): + '''Add a progressbar to the multibar''' + if value.label != key: # pragma: no branch + value.label = key + value.fd = stream.LastLineStream(self.fd) + value.paused = True + value.print = self.print + + # Just in case someone is using a progressbar with a custom + # constructor and forgot to call the super constructor + if value.index == -1: + value.index = next(value._index_counter) + + super().__setitem__(key, value) + + def __delitem__(self, key): + '''Remove a progressbar from the multibar''' + super().__delitem__(key) + self._finished_at.pop(key, None) + self._labeled.discard(key) + + def __getitem__(self, item): + '''Get (and create if needed) a progressbar from the multibar''' + try: + return super().__getitem__(item) + except KeyError: + progress = bar.ProgressBar(**self.progressbar_kwargs) + self[item] = progress + return progress + + def _label_bar(self, bar: bar.ProgressBar): + if bar in self._labeled: # pragma: no branch + return + + assert bar.widgets, 'Cannot prepend label to empty progressbar' + self._labeled.add(bar) + + if self.prepend_label: # pragma: no branch + bar.widgets.insert(0, self.label_format.format(label=bar.label)) + + if self.append_label and bar not in self._labeled: # pragma: no branch + bar.widgets.append(self.label_format.format(label=bar.label)) + + def render(self, flush: bool = True, force: bool = False): + '''Render the multibar to the given stream''' + now = timeit.default_timer() + expired = now - self.remove_finished if self.remove_finished else None + output = [] + for bar_ in self.get_sorted_bars(): + if not bar_.started() and not self.show_initial: + continue + + def update(force=True, write=True): + self._label_bar(bar_) + bar_.update(force=force) + if write: + output.append(bar_.fd.line) + + if bar_.finished(): + if bar_ not in self._finished_at: + self._finished_at[bar_] = now + # Force update to get the finished format + update(write=False) + + if self.remove_finished and expired >= self._finished_at[bar_]: + del self[bar_.label] + continue + + if not self.show_finished: + continue + + if bar_.finished(): + if self.finished_format is None: + update(force=False) + else: + output.append( + self.finished_format.format( + label=bar_.label + ) + ) + elif bar_.started(): + update() + else: + if self.initial_format is None: + bar_.start() + update() + else: + output.append(self.initial_format.format(label=bar_.label)) + + with self._print_lock: + # Clear the previous output if progressbars have been removed + for i in range(len(output), len(self._previous_output)): + self._buffer.write(terminal.clear_line(i + 1)) + + # Add empty lines to the end of the output if progressbars have + # been added + for i in range(len(self._previous_output), len(output)): + # Adding a new line so we don't overwrite previous output + self._buffer.write('\n') + + for i, (previous, current) in enumerate( + itertools.zip_longest( + self._previous_output, output, fillvalue='' + ) + ): + if previous != current or force: + self.print( + '\r' + current.strip(), + offset=i + 1, + end='', + clear=False, + flush=False, + ) + + self._previous_output = output + + if flush: + self.flush() + + def print( + self, *args, end='\n', offset=None, flush=True, clear=True, **kwargs + ): + ''' + Print to the progressbar stream without overwriting the progressbars + + Args: + end: The string to append to the end of the output + offset: The number of lines to offset the output by. If None, the + output will be printed above the progressbars + flush: Whether to flush the output to the stream + clear: If True, the line will be cleared before printing. + **kwargs: Additional keyword arguments to pass to print + ''' + with self._print_lock: + if offset is None: + offset = len(self._previous_output) + + if not clear: + self._buffer.write(terminal.PREVIOUS_LINE(offset)) + + if clear: + self._buffer.write(terminal.PREVIOUS_LINE(offset)) + self._buffer.write(terminal.CLEAR_LINE_ALL()) + + print(*args, **kwargs, file=self._buffer, end=end) + + if clear: + self._buffer.write(terminal.CLEAR_SCREEN_TILL_END()) + for line in self._previous_output: + self._buffer.write(line.strip()) + self._buffer.write('\n') + + else: + self._buffer.write(terminal.NEXT_LINE(offset)) + + if flush: + self.flush() + + def flush(self): + self.fd.write(self._buffer.getvalue()) + self._buffer.truncate(0) + self.fd.flush() + + def run(self, join=True): + ''' + Start the multibar render loop and run the progressbars until they + have force _thread_finished + ''' + while not self._thread_finished.is_set(): + self.render() + time.sleep(self.update_interval) + + if join or self._thread_closed.is_set(): + # If the thread is closed, we need to check if force + # progressbars + # have finished. If they have, we can exit the loop + for bar_ in self.values(): + if not bar_.finished(): + break + else: + # Render one last time to make sure the progressbars are + # correctly finished + self.render(force=True) + return + + def start(self): + assert not self._thread, 'Multibar already started' + self._thread_closed.set() + self._thread = threading.Thread(target=self.run, args=(False,)) + self._thread.start() + + def join(self, timeout=None): + if self._thread is not None: + self._thread_closed.set() + self._thread.join(timeout=timeout) + self._thread = None + + def stop(self, timeout: float | None = None): + self._thread_finished.set() + self.join(timeout=timeout) + + def get_sorted_bars(self): + return sorted( + self.values(), + key=self.sort_keyfunc, + reverse=self.sort_reverse, + ) + + def __enter__(self): + self.start() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.join() diff --git a/progressbar/shortcuts.py b/progressbar/shortcuts.py index 9e1502dd..eb30b715 100644 --- a/progressbar/shortcuts.py +++ b/progressbar/shortcuts.py @@ -19,5 +19,4 @@ def progressbar( **kwargs ) - for result in progressbar(iterator): - yield result + yield from progressbar(iterator) diff --git a/progressbar/terminal/__init__.py b/progressbar/terminal/__init__.py new file mode 100644 index 00000000..4b40b38c --- /dev/null +++ b/progressbar/terminal/__init__.py @@ -0,0 +1 @@ +from .base import * # noqa diff --git a/progressbar/terminal/base.py b/progressbar/terminal/base.py new file mode 100644 index 00000000..8df2e75a --- /dev/null +++ b/progressbar/terminal/base.py @@ -0,0 +1,535 @@ +from __future__ import annotations + +import abc +import collections +import colorsys +import enum +import os +import threading +from collections import defaultdict + +from python_utils import converters, types + +from .os_specific import getch +from .. import base + +ESC = '\x1B' + + +class CSI: + _code: str + _template = ESC + '[{args}{code}' + + def __init__(self, code, *default_args): + self._code = code + self._default_args = default_args + + def __call__(self, *args): + return self._template.format( + args=';'.join(map(str, args or self._default_args)), + code=self._code, + ) + + def __str__(self): + return self() + + +class CSINoArg(CSI): + def __call__(self): + return super().__call__() + + +#: Cursor Position [row;column] (default = [1,1]) +CUP = CSI('H', 1, 1) + +#: Cursor Up Ps Times (default = 1) (CUU) +UP = CSI('A', 1) + +#: Cursor Down Ps Times (default = 1) (CUD) +DOWN = CSI('B', 1) + +#: Cursor Forward Ps Times (default = 1) (CUF) +RIGHT = CSI('C', 1) + +#: Cursor Backward Ps Times (default = 1) (CUB) +LEFT = CSI('D', 1) + +#: Cursor Next Line Ps Times (default = 1) (CNL) +#: Same as Cursor Down Ps Times +NEXT_LINE = CSI('E', 1) + +#: Cursor Preceding Line Ps Times (default = 1) (CPL) +#: Same as Cursor Up Ps Times +PREVIOUS_LINE = CSI('F', 1) + +#: Cursor Character Absolute [column] (default = [row,1]) (CHA) +COLUMN = CSI('G', 1) + +#: Erase in Display (ED) +CLEAR_SCREEN = CSI('J', 0) + +#: Erase till end of screen +CLEAR_SCREEN_TILL_END = CSINoArg('0J') + +#: Erase till start of screen +CLEAR_SCREEN_TILL_START = CSINoArg('1J') + +#: Erase whole screen +CLEAR_SCREEN_ALL = CSINoArg('2J') + +#: Erase whole screen and history +CLEAR_SCREEN_ALL_AND_HISTORY = CSINoArg('3J') + +#: Erase in Line (EL) +CLEAR_LINE_ALL = CSI('K') + +#: Erase in Line from Cursor to End of Line (default) +CLEAR_LINE_RIGHT = CSINoArg('0K') + +#: Erase in Line from Cursor to Beginning of Line +CLEAR_LINE_LEFT = CSINoArg('1K') + +#: Erase Line containing Cursor +CLEAR_LINE = CSINoArg('2K') + +#: Scroll up Ps lines (default = 1) (SU) +#: Scroll down Ps lines (default = 1) (SD) +SCROLL_UP = CSI('S') +SCROLL_DOWN = CSI('T') + +#: Save Cursor Position (SCP) +SAVE_CURSOR = CSINoArg('s') + +#: Restore Cursor Position (RCP) +RESTORE_CURSOR = CSINoArg('u') + +#: Cursor Visibility (DECTCEM) +HIDE_CURSOR = CSINoArg('?25l') +SHOW_CURSOR = CSINoArg('?25h') + + +# +# UP = CSI + '{n}A' # Cursor Up +# DOWN = CSI + '{n}B' # Cursor Down +# RIGHT = CSI + '{n}C' # Cursor Forward +# LEFT = CSI + '{n}D' # Cursor Backward +# NEXT = CSI + '{n}E' # Cursor Next Line +# PREV = CSI + '{n}F' # Cursor Previous Line +# MOVE_COLUMN = CSI + '{n}G' # Cursor Horizontal Absolute +# MOVE = CSI + '{row};{column}H' # Cursor Position [row;column] (default = [ +# 1,1]) +# +# CLEAR = CSI + '{n}J' # Clear (part of) the screen +# CLEAR_BOTTOM = CLEAR.format(n=0) # Clear from cursor to end of screen +# CLEAR_TOP = CLEAR.format(n=1) # Clear from cursor to beginning of screen +# CLEAR_SCREEN = CLEAR.format(n=2) # Clear Screen +# CLEAR_WIPE = CLEAR.format(n=3) # Clear Screen and scrollback buffer +# +# CLEAR_LINE = CSI + '{n}K' # Erase in Line +# CLEAR_LINE_RIGHT = CLEAR_LINE.format(n=0) # Clear from cursor to end of line +# CLEAR_LINE_LEFT = CLEAR_LINE.format(n=1) # Clear from cursor to beginning +# of line +# CLEAR_LINE_ALL = CLEAR_LINE.format(n=2) # Clear Line + + +def clear_line(n): + return UP(n) + CLEAR_LINE_ALL() + DOWN(n) + + +class ColorSupport(enum.IntEnum): + '''Color support for the terminal.''' + + NONE = 0 + XTERM = 16 + XTERM_256 = 256 + XTERM_TRUECOLOR = 16777216 + + @classmethod + def from_env(cls): + '''Get the color support from the environment. + + If any of the environment variables contain `24bit` or `truecolor`, + we will enable true color/24 bit support. If they contain `256`, we + will enable 256 color/8 bit support. If they contain `xterm`, we will + enable 16 color support. Otherwise, we will assume no color support. + + If `JUPYTER_COLUMNS` or `JUPYTER_LINES` is set, we will assume true + color support. + + Note that the highest available value will be used! Having + `COLORTERM=truecolor` will override `TERM=xterm-256color`. + ''' + variables = ( + 'FORCE_COLOR', + 'PROGRESSBAR_ENABLE_COLORS', + 'COLORTERM', + 'TERM', + ) + + if os.environ.get('JUPYTER_COLUMNS') or os.environ.get( + 'JUPYTER_LINES' + ): + # Jupyter notebook always supports true color. + return cls.XTERM_TRUECOLOR + + support = cls.NONE + for variable in variables: + value = os.environ.get(variable) + if value is None: + continue + elif value in {'truecolor', '24bit'}: + # Truecolor support, we don't need to check anything else. + support = cls.XTERM_TRUECOLOR + break + elif '256' in value: + support = max(cls.XTERM_256, support) + elif value == 'xterm': + support = max(cls.XTERM, support) + + return support + + +color_support = ColorSupport.from_env() + + +# Report Cursor Position (CPR), response = [row;column] as row;columnR +class _CPR(str): + _response_lock = threading.Lock() + + def __call__(self, stream): + res = '' + + with self._response_lock: + stream.write(str(self)) + stream.flush() + + while not res.endswith('R'): + char = getch() + + if char is not None: + res += char + + res = res[2:-1].split(';') + + res = tuple(int(item) if item.isdigit() else item for item in res) + + return res[0] if len(res) == 1 else res + + def row(self, stream): + row, _ = self(stream) + return row + + def column(self, stream): + _, column = self(stream) + return column + + +class RGB(collections.namedtuple('RGB', ['red', 'green', 'blue'])): + __slots__ = () + + def __str__(self): + return self.rgb + + @property + def rgb(self): + return f'rgb({self.red}, {self.green}, {self.blue})' + + @property + def hex(self): + return f'#{self.red:02x}{self.green:02x}{self.blue:02x}' + + @property + def to_ansi_16(self): + # Using int instead of round because it maps slightly better + red = int(self.red / 255) + green = int(self.green / 255) + blue = int(self.blue / 255) + return (blue << 2) | (green << 1) | red + + @property + def to_ansi_256(self): + red = round(self.red / 255 * 5) + green = round(self.green / 255 * 5) + blue = round(self.blue / 255 * 5) + return 16 + 36 * red + 6 * green + blue + + def interpolate(self, end: RGB, step: float) -> RGB: + return RGB( + int(self.red + (end.red - self.red) * step), + int(self.green + (end.green - self.green) * step), + int(self.blue + (end.blue - self.blue) * step), + ) + + +class HLS(collections.namedtuple('HLS', ['hue', 'lightness', 'saturation'])): + __slots__ = () + + @classmethod + def from_rgb(cls, rgb: RGB) -> HLS: + return cls( + *colorsys.rgb_to_hls( + rgb.red / 255, rgb.green / 255, rgb.blue / 255 + ) + ) + + def interpolate(self, end: HLS, step: float) -> HLS: + return HLS( + self.hue + (end.hue - self.hue) * step, + self.lightness + (end.lightness - self.lightness) * step, + self.saturation + (end.saturation - self.saturation) * step, + ) + + +class ColorBase(abc.ABC): + def get_color(self, value: float) -> Color: + raise NotImplementedError() + + +class Color( + collections.namedtuple( + 'Color', + [ + 'rgb', + 'hls', + 'name', + 'xterm', + ], + ), + ColorBase, +): + ''' + Color base class + + This class contains the colors in RGB (Red, Green, Blue), HLS (Hue, + Lightness, Saturation) and Xterm (8-bit) formats. It also contains the + color name. + + To make a custom color the only required arguments are the RGB values. + The other values will be automatically interpolated from that if needed, + but you can be more explicity if you wish. + ''' + + __slots__ = () + + def __call__(self, value: str) -> str: + return self.fg(value) + + @property + def fg(self): + return SGRColor(self, 38, 39) + + @property + def bg(self): + return SGRColor(self, 48, 49) + + @property + def underline(self): + return SGRColor(self, 58, 59) + + @property + def ansi(self) -> types.Optional[str]: + if color_support is ColorSupport.XTERM_TRUECOLOR: + return f'2;{self.rgb.red};{self.rgb.green};{self.rgb.blue}' + + if self.xterm: + color = self.xterm + elif color_support is ColorSupport.XTERM_256: + color = self.rgb.to_ansi_256 + elif color_support is ColorSupport.XTERM: + color = self.rgb.to_ansi_16 + else: + return None + + return f'5;{color}' + + def interpolate(self, end: Color, step: float) -> Color: + return Color( + self.rgb.interpolate(end.rgb, step), + self.hls.interpolate(end.hls, step), + self.name if step < 0.5 else end.name, + self.xterm if step < 0.5 else end.xterm, + ) + + def __str__(self): + return self.name + + def __repr__(self): + return f'{self.__class__.__name__}({self.name!r})' + + def __hash__(self): + return hash(self.rgb) + + +class Colors: + by_name: defaultdict[str, types.List[Color]] = collections.defaultdict( + list + ) + by_lowername: defaultdict[ + str, types.List[Color] + ] = collections.defaultdict(list) + by_hex: defaultdict[str, types.List[Color]] = collections.defaultdict(list) + by_rgb: defaultdict[RGB, types.List[Color]] = collections.defaultdict(list) + by_hls: defaultdict[HLS, types.List[Color]] = collections.defaultdict(list) + by_xterm: dict[int, Color] = dict() + + @classmethod + def register( + cls, + rgb: RGB, + hls: types.Optional[HLS] = None, + name: types.Optional[str] = None, + xterm: types.Optional[int] = None, + ) -> Color: + color = Color(rgb, hls, name, xterm) + + if name: + cls.by_name[name].append(color) + cls.by_lowername[name.lower()].append(color) + + if hls is None: + hls = HLS.from_rgb(rgb) + + cls.by_hex[rgb.hex].append(color) + cls.by_rgb[rgb].append(color) + cls.by_hls[hls].append(color) + + if xterm is not None: + cls.by_xterm[xterm] = color + + return color + + @classmethod + def interpolate(cls, color_a: Color, color_b: Color, step: float) -> Color: + return color_a.interpolate(color_b, step) + + +class ColorGradient(ColorBase): + def __init__(self, *colors: Color, interpolate=Colors.interpolate): + assert colors + self.colors = colors + self.interpolate = interpolate + + def __call__(self, value: float): + return self.get_color(value) + + def get_color(self, value: float) -> Color: + 'Map a value from 0 to 1 to a color' + if ( + value is base.Undefined + or value is base.UnknownLength + or value <= 0 + ): + return self.colors[0] + elif value >= 1: + return self.colors[-1] + + max_color_idx = len(self.colors) - 1 + if max_color_idx == 0: + return self.colors[0] + elif self.interpolate: + index = round(converters.remap(value, 0, 1, 0, max_color_idx - 1)) + step = converters.remap( + value, + index / (max_color_idx), + (index + 1) / (max_color_idx), + 0, + 1, + ) + color = self.interpolate( + self.colors[index], + self.colors[index + 1], + float(step), + ) + else: + index = round(converters.remap(value, 0, 1, 0, max_color_idx)) + color = self.colors[index] + + return color + + +OptionalColor = types.Union[Color, ColorGradient, None] + + +def get_color(value: float, color: OptionalColor) -> Color | None: + if isinstance(color, ColorGradient): + color = color(value) + return color + + +def apply_colors( + text: str, + percentage: float | None = None, + *, + fg: OptionalColor = None, + bg: OptionalColor = None, + fg_none: Color | None = None, + bg_none: Color | None = None, + **kwargs: types.Any, +) -> str: + if fg is None and bg is None: + return text + + if percentage is None: + if fg_none is not None: + text = fg_none.fg(text) + if bg_none is not None: + text = bg_none.bg(text) + else: + fg = get_color(percentage * 0.01, fg) + bg = get_color(percentage * 0.01, bg) + + if fg is not None: + text = fg.fg(text) + if bg is not None: + text = bg.bg(text) + + return text + + +class SGR(CSI): + _start_code: int + _end_code: int + _code = 'm' + __slots__ = '_start_code', '_end_code' + + def __init__(self, start_code: int, end_code: int): + self._start_code = start_code + self._end_code = end_code + + @property + def _start_template(self): + return super().__call__(self._start_code) + + @property + def _end_template(self): + return super().__call__(self._end_code) + + def __call__(self, text): + return self._start_template + text + self._end_template + + +class SGRColor(SGR): + __slots__ = '_color', '_start_code', '_end_code' + + def __init__(self, color: Color, start_code: int, end_code: int): + self._color = color + super().__init__(start_code, end_code) + + @property + def _start_template(self): + return CSI.__call__(self, self._start_code, self._color.ansi) + + +encircled = SGR(52, 54) +framed = SGR(51, 54) +overline = SGR(53, 55) +bold = SGR(1, 22) +gothic = SGR(20, 10) +italic = SGR(3, 23) +strike_through = SGR(9, 29) +fast_blink = SGR(6, 25) +slow_blink = SGR(5, 25) +underline = SGR(4, 24) +double_underline = SGR(21, 24) +faint = SGR(2, 22) +inverse = SGR(7, 27) diff --git a/progressbar/terminal/colors.py b/progressbar/terminal/colors.py new file mode 100644 index 00000000..f05328a6 --- /dev/null +++ b/progressbar/terminal/colors.py @@ -0,0 +1,599 @@ +# Based on: https://www.ditig.com/256-colors-cheat-sheet +import os + +from progressbar.terminal.base import ColorGradient, Colors, HLS, RGB + +black = Colors.register(RGB(0, 0, 0), HLS(0, 0, 0), 'Black', 0) +maroon = Colors.register(RGB(128, 0, 0), HLS(100, 0, 25), 'Maroon', 1) +green = Colors.register(RGB(0, 128, 0), HLS(100, 120, 25), 'Green', 2) +olive = Colors.register(RGB(128, 128, 0), HLS(100, 60, 25), 'Olive', 3) +navy = Colors.register(RGB(0, 0, 128), HLS(100, 240, 25), 'Navy', 4) +purple = Colors.register(RGB(128, 0, 128), HLS(100, 300, 25), 'Purple', 5) +teal = Colors.register(RGB(0, 128, 128), HLS(100, 180, 25), 'Teal', 6) +silver = Colors.register(RGB(192, 192, 192), HLS(0, 0, 75), 'Silver', 7) +grey = Colors.register(RGB(128, 128, 128), HLS(0, 0, 50), 'Grey', 8) +red = Colors.register(RGB(255, 0, 0), HLS(100, 0, 50), 'Red', 9) +lime = Colors.register(RGB(0, 255, 0), HLS(100, 120, 50), 'Lime', 10) +yellow = Colors.register(RGB(255, 255, 0), HLS(100, 60, 50), 'Yellow', 11) +blue = Colors.register(RGB(0, 0, 255), HLS(100, 240, 50), 'Blue', 12) +fuchsia = Colors.register(RGB(255, 0, 255), HLS(100, 300, 50), 'Fuchsia', 13) +aqua = Colors.register(RGB(0, 255, 255), HLS(100, 180, 50), 'Aqua', 14) +white = Colors.register(RGB(255, 255, 255), HLS(0, 0, 100), 'White', 15) +grey0 = Colors.register(RGB(0, 0, 0), HLS(0, 0, 0), 'Grey0', 16) +navyBlue = Colors.register(RGB(0, 0, 95), HLS(100, 240, 18), 'NavyBlue', 17) +darkBlue = Colors.register(RGB(0, 0, 135), HLS(100, 240, 26), 'DarkBlue', 18) +blue3 = Colors.register(RGB(0, 0, 175), HLS(100, 240, 34), 'Blue3', 19) +blue3 = Colors.register(RGB(0, 0, 215), HLS(100, 240, 42), 'Blue3', 20) +blue1 = Colors.register(RGB(0, 0, 255), HLS(100, 240, 50), 'Blue1', 21) +darkGreen = Colors.register(RGB(0, 95, 0), HLS(100, 120, 18), 'DarkGreen', 22) +deepSkyBlue4 = Colors.register( + RGB(0, 95, 95), HLS(100, 180, 18), 'DeepSkyBlue4', 23 +) +deepSkyBlue4 = Colors.register( + RGB(0, 95, 135), HLS(100, 97, 26), 'DeepSkyBlue4', 24 +) +deepSkyBlue4 = Colors.register( + RGB(0, 95, 175), HLS(100, 7, 34), 'DeepSkyBlue4', 25 +) +dodgerBlue3 = Colors.register( + RGB(0, 95, 215), HLS(100, 13, 42), 'DodgerBlue3', 26 +) +dodgerBlue2 = Colors.register( + RGB(0, 95, 255), HLS(100, 17, 50), 'DodgerBlue2', 27 +) +green4 = Colors.register(RGB(0, 135, 0), HLS(100, 120, 26), 'Green4', 28) +springGreen4 = Colors.register( + RGB(0, 135, 95), HLS(100, 62, 26), 'SpringGreen4', 29 +) +turquoise4 = Colors.register( + RGB(0, 135, 135), HLS(100, 180, 26), 'Turquoise4', 30 +) +deepSkyBlue3 = Colors.register( + RGB(0, 135, 175), HLS(100, 93, 34), 'DeepSkyBlue3', 31 +) +deepSkyBlue3 = Colors.register( + RGB(0, 135, 215), HLS(100, 2, 42), 'DeepSkyBlue3', 32 +) +dodgerBlue1 = Colors.register( + RGB(0, 135, 255), HLS(100, 8, 50), 'DodgerBlue1', 33 +) +green3 = Colors.register(RGB(0, 175, 0), HLS(100, 120, 34), 'Green3', 34) +springGreen3 = Colors.register( + RGB(0, 175, 95), HLS(100, 52, 34), 'SpringGreen3', 35 +) +darkCyan = Colors.register(RGB(0, 175, 135), HLS(100, 66, 34), 'DarkCyan', 36) +lightSeaGreen = Colors.register( + RGB(0, 175, 175), HLS(100, 180, 34), 'LightSeaGreen', 37 +) +deepSkyBlue2 = Colors.register( + RGB(0, 175, 215), HLS(100, 91, 42), 'DeepSkyBlue2', 38 +) +deepSkyBlue1 = Colors.register( + RGB(0, 175, 255), HLS(100, 98, 50), 'DeepSkyBlue1', 39 +) +green3 = Colors.register(RGB(0, 215, 0), HLS(100, 120, 42), 'Green3', 40) +springGreen3 = Colors.register( + RGB(0, 215, 95), HLS(100, 46, 42), 'SpringGreen3', 41 +) +springGreen2 = Colors.register( + RGB(0, 215, 135), HLS(100, 57, 42), 'SpringGreen2', 42 +) +cyan3 = Colors.register(RGB(0, 215, 175), HLS(100, 68, 42), 'Cyan3', 43) +darkTurquoise = Colors.register( + RGB(0, 215, 215), HLS(100, 180, 42), 'DarkTurquoise', 44 +) +turquoise2 = Colors.register( + RGB(0, 215, 255), HLS(100, 89, 50), 'Turquoise2', 45 +) +green1 = Colors.register(RGB(0, 255, 0), HLS(100, 120, 50), 'Green1', 46) +springGreen2 = Colors.register( + RGB(0, 255, 95), HLS(100, 42, 50), 'SpringGreen2', 47 +) +springGreen1 = Colors.register( + RGB(0, 255, 135), HLS(100, 51, 50), 'SpringGreen1', 48 +) +mediumSpringGreen = Colors.register( + RGB(0, 255, 175), HLS(100, 61, 50), 'MediumSpringGreen', 49 +) +cyan2 = Colors.register(RGB(0, 255, 215), HLS(100, 70, 50), 'Cyan2', 50) +cyan1 = Colors.register(RGB(0, 255, 255), HLS(100, 180, 50), 'Cyan1', 51) +darkRed = Colors.register(RGB(95, 0, 0), HLS(100, 0, 18), 'DarkRed', 52) +deepPink4 = Colors.register(RGB(95, 0, 95), HLS(100, 300, 18), 'DeepPink4', 53) +purple4 = Colors.register(RGB(95, 0, 135), HLS(100, 82, 26), 'Purple4', 54) +purple4 = Colors.register(RGB(95, 0, 175), HLS(100, 72, 34), 'Purple4', 55) +purple3 = Colors.register(RGB(95, 0, 215), HLS(100, 66, 42), 'Purple3', 56) +blueViolet = Colors.register( + RGB(95, 0, 255), HLS(100, 62, 50), 'BlueViolet', 57 +) +orange4 = Colors.register(RGB(95, 95, 0), HLS(100, 60, 18), 'Orange4', 58) +grey37 = Colors.register(RGB(95, 95, 95), HLS(0, 0, 37), 'Grey37', 59) +mediumPurple4 = Colors.register( + RGB(95, 95, 135), HLS(17, 240, 45), 'MediumPurple4', 60 +) +slateBlue3 = Colors.register( + RGB(95, 95, 175), HLS(33, 240, 52), 'SlateBlue3', 61 +) +slateBlue3 = Colors.register( + RGB(95, 95, 215), HLS(60, 240, 60), 'SlateBlue3', 62 +) +royalBlue1 = Colors.register( + RGB(95, 95, 255), HLS(100, 240, 68), 'RoyalBlue1', 63 +) +chartreuse4 = Colors.register( + RGB(95, 135, 0), HLS(100, 7, 26), 'Chartreuse4', 64 +) +darkSeaGreen4 = Colors.register( + RGB(95, 135, 95), HLS(17, 120, 45), 'DarkSeaGreen4', 65 +) +paleTurquoise4 = Colors.register( + RGB(95, 135, 135), HLS(17, 180, 45), 'PaleTurquoise4', 66 +) +steelBlue = Colors.register( + RGB(95, 135, 175), HLS(33, 210, 52), 'SteelBlue', 67 +) +steelBlue3 = Colors.register( + RGB(95, 135, 215), HLS(60, 220, 60), 'SteelBlue3', 68 +) +cornflowerBlue = Colors.register( + RGB(95, 135, 255), HLS(100, 225, 68), 'CornflowerBlue', 69 +) +chartreuse3 = Colors.register( + RGB(95, 175, 0), HLS(100, 7, 34), 'Chartreuse3', 70 +) +darkSeaGreen4 = Colors.register( + RGB(95, 175, 95), HLS(33, 120, 52), 'DarkSeaGreen4', 71 +) +cadetBlue = Colors.register( + RGB(95, 175, 135), HLS(33, 150, 52), 'CadetBlue', 72 +) +cadetBlue = Colors.register( + RGB(95, 175, 175), HLS(33, 180, 52), 'CadetBlue', 73 +) +skyBlue3 = Colors.register(RGB(95, 175, 215), HLS(60, 200, 60), 'SkyBlue3', 74) +steelBlue1 = Colors.register( + RGB(95, 175, 255), HLS(100, 210, 68), 'SteelBlue1', 75 +) +chartreuse3 = Colors.register( + RGB(95, 215, 0), HLS(100, 3, 42), 'Chartreuse3', 76 +) +paleGreen3 = Colors.register( + RGB(95, 215, 95), HLS(60, 120, 60), 'PaleGreen3', 77 +) +seaGreen3 = Colors.register( + RGB(95, 215, 135), HLS(60, 140, 60), 'SeaGreen3', 78 +) +aquamarine3 = Colors.register( + RGB(95, 215, 175), HLS(60, 160, 60), 'Aquamarine3', 79 +) +mediumTurquoise = Colors.register( + RGB(95, 215, 215), HLS(60, 180, 60), 'MediumTurquoise', 80 +) +steelBlue1 = Colors.register( + RGB(95, 215, 255), HLS(100, 195, 68), 'SteelBlue1', 81 +) +chartreuse2 = Colors.register( + RGB(95, 255, 0), HLS(100, 7, 50), 'Chartreuse2', 82 +) +seaGreen2 = Colors.register( + RGB(95, 255, 95), HLS(100, 120, 68), 'SeaGreen2', 83 +) +seaGreen1 = Colors.register( + RGB(95, 255, 135), HLS(100, 135, 68), 'SeaGreen1', 84 +) +seaGreen1 = Colors.register( + RGB(95, 255, 175), HLS(100, 150, 68), 'SeaGreen1', 85 +) +aquamarine1 = Colors.register( + RGB(95, 255, 215), HLS(100, 165, 68), 'Aquamarine1', 86 +) +darkSlateGray2 = Colors.register( + RGB(95, 255, 255), HLS(100, 180, 68), 'DarkSlateGray2', 87 +) +darkRed = Colors.register(RGB(135, 0, 0), HLS(100, 0, 26), 'DarkRed', 88) +deepPink4 = Colors.register(RGB(135, 0, 95), HLS(100, 17, 26), 'DeepPink4', 89) +darkMagenta = Colors.register( + RGB(135, 0, 135), HLS(100, 300, 26), 'DarkMagenta', 90 +) +darkMagenta = Colors.register( + RGB(135, 0, 175), HLS(100, 86, 34), 'DarkMagenta', 91 +) +darkViolet = Colors.register( + RGB(135, 0, 215), HLS(100, 77, 42), 'DarkViolet', 92 +) +purple = Colors.register(RGB(135, 0, 255), HLS(100, 71, 50), 'Purple', 93) +orange4 = Colors.register(RGB(135, 95, 0), HLS(100, 2, 26), 'Orange4', 94) +lightPink4 = Colors.register( + RGB(135, 95, 95), HLS(17, 0, 45), 'LightPink4', 95 +) +plum4 = Colors.register(RGB(135, 95, 135), HLS(17, 300, 45), 'Plum4', 96) +mediumPurple3 = Colors.register( + RGB(135, 95, 175), HLS(33, 270, 52), 'MediumPurple3', 97 +) +mediumPurple3 = Colors.register( + RGB(135, 95, 215), HLS(60, 260, 60), 'MediumPurple3', 98 +) +slateBlue1 = Colors.register( + RGB(135, 95, 255), HLS(100, 255, 68), 'SlateBlue1', 99 +) +yellow4 = Colors.register(RGB(135, 135, 0), HLS(100, 60, 26), 'Yellow4', 100) +wheat4 = Colors.register(RGB(135, 135, 95), HLS(17, 60, 45), 'Wheat4', 101) +grey53 = Colors.register(RGB(135, 135, 135), HLS(0, 0, 52), 'Grey53', 102) +lightSlateGrey = Colors.register( + RGB(135, 135, 175), HLS(20, 240, 60), 'LightSlateGrey', 103 +) +mediumPurple = Colors.register( + RGB(135, 135, 215), HLS(50, 240, 68), 'MediumPurple', 104 +) +lightSlateBlue = Colors.register( + RGB(135, 135, 255), HLS(100, 240, 76), 'LightSlateBlue', 105 +) +yellow4 = Colors.register(RGB(135, 175, 0), HLS(100, 3, 34), 'Yellow4', 106) +darkOliveGreen3 = Colors.register( + RGB(135, 175, 95), HLS(33, 90, 52), 'DarkOliveGreen3', 107 +) +darkSeaGreen = Colors.register( + RGB(135, 175, 135), HLS(20, 120, 60), 'DarkSeaGreen', 108 +) +lightSkyBlue3 = Colors.register( + RGB(135, 175, 175), HLS(20, 180, 60), 'LightSkyBlue3', 109 +) +lightSkyBlue3 = Colors.register( + RGB(135, 175, 215), HLS(50, 210, 68), 'LightSkyBlue3', 110 +) +skyBlue2 = Colors.register( + RGB(135, 175, 255), HLS(100, 220, 76), 'SkyBlue2', 111 +) +chartreuse2 = Colors.register( + RGB(135, 215, 0), HLS(100, 2, 42), 'Chartreuse2', 112 +) +darkOliveGreen3 = Colors.register( + RGB(135, 215, 95), HLS(60, 100, 60), 'DarkOliveGreen3', 113 +) +paleGreen3 = Colors.register( + RGB(135, 215, 135), HLS(50, 120, 68), 'PaleGreen3', 114 +) +darkSeaGreen3 = Colors.register( + RGB(135, 215, 175), HLS(50, 150, 68), 'DarkSeaGreen3', 115 +) +darkSlateGray3 = Colors.register( + RGB(135, 215, 215), HLS(50, 180, 68), 'DarkSlateGray3', 116 +) +skyBlue1 = Colors.register( + RGB(135, 215, 255), HLS(100, 200, 76), 'SkyBlue1', 117 +) +chartreuse1 = Colors.register( + RGB(135, 255, 0), HLS(100, 8, 50), 'Chartreuse1', 118 +) +lightGreen = Colors.register( + RGB(135, 255, 95), HLS(100, 105, 68), 'LightGreen', 119 +) +lightGreen = Colors.register( + RGB(135, 255, 135), HLS(100, 120, 76), 'LightGreen', 120 +) +paleGreen1 = Colors.register( + RGB(135, 255, 175), HLS(100, 140, 76), 'PaleGreen1', 121 +) +aquamarine1 = Colors.register( + RGB(135, 255, 215), HLS(100, 160, 76), 'Aquamarine1', 122 +) +darkSlateGray1 = Colors.register( + RGB(135, 255, 255), HLS(100, 180, 76), 'DarkSlateGray1', 123 +) +red3 = Colors.register(RGB(175, 0, 0), HLS(100, 0, 34), 'Red3', 124) +deepPink4 = Colors.register( + RGB(175, 0, 95), HLS(100, 27, 34), 'DeepPink4', 125 +) +mediumVioletRed = Colors.register( + RGB(175, 0, 135), HLS(100, 13, 34), 'MediumVioletRed', 126 +) +magenta3 = Colors.register( + RGB(175, 0, 175), HLS(100, 300, 34), 'Magenta3', 127 +) +darkViolet = Colors.register( + RGB(175, 0, 215), HLS(100, 88, 42), 'DarkViolet', 128 +) +purple = Colors.register(RGB(175, 0, 255), HLS(100, 81, 50), 'Purple', 129) +darkOrange3 = Colors.register( + RGB(175, 95, 0), HLS(100, 2, 34), 'DarkOrange3', 130 +) +indianRed = Colors.register(RGB(175, 95, 95), HLS(33, 0, 52), 'IndianRed', 131) +hotPink3 = Colors.register( + RGB(175, 95, 135), HLS(33, 330, 52), 'HotPink3', 132 +) +mediumOrchid3 = Colors.register( + RGB(175, 95, 175), HLS(33, 300, 52), 'MediumOrchid3', 133 +) +mediumOrchid = Colors.register( + RGB(175, 95, 215), HLS(60, 280, 60), 'MediumOrchid', 134 +) +mediumPurple2 = Colors.register( + RGB(175, 95, 255), HLS(100, 270, 68), 'MediumPurple2', 135 +) +darkGoldenrod = Colors.register( + RGB(175, 135, 0), HLS(100, 6, 34), 'DarkGoldenrod', 136 +) +lightSalmon3 = Colors.register( + RGB(175, 135, 95), HLS(33, 30, 52), 'LightSalmon3', 137 +) +rosyBrown = Colors.register( + RGB(175, 135, 135), HLS(20, 0, 60), 'RosyBrown', 138 +) +grey63 = Colors.register(RGB(175, 135, 175), HLS(20, 300, 60), 'Grey63', 139) +mediumPurple2 = Colors.register( + RGB(175, 135, 215), HLS(50, 270, 68), 'MediumPurple2', 140 +) +mediumPurple1 = Colors.register( + RGB(175, 135, 255), HLS(100, 260, 76), 'MediumPurple1', 141 +) +gold3 = Colors.register(RGB(175, 175, 0), HLS(100, 60, 34), 'Gold3', 142) +darkKhaki = Colors.register( + RGB(175, 175, 95), HLS(33, 60, 52), 'DarkKhaki', 143 +) +navajoWhite3 = Colors.register( + RGB(175, 175, 135), HLS(20, 60, 60), 'NavajoWhite3', 144 +) +grey69 = Colors.register(RGB(175, 175, 175), HLS(0, 0, 68), 'Grey69', 145) +lightSteelBlue3 = Colors.register( + RGB(175, 175, 215), HLS(33, 240, 76), 'LightSteelBlue3', 146 +) +lightSteelBlue = Colors.register( + RGB(175, 175, 255), HLS(100, 240, 84), 'LightSteelBlue', 147 +) +yellow3 = Colors.register(RGB(175, 215, 0), HLS(100, 1, 42), 'Yellow3', 148) +darkOliveGreen3 = Colors.register( + RGB(175, 215, 95), HLS(60, 80, 60), 'DarkOliveGreen3', 149 +) +darkSeaGreen3 = Colors.register( + RGB(175, 215, 135), HLS(50, 90, 68), 'DarkSeaGreen3', 150 +) +darkSeaGreen2 = Colors.register( + RGB(175, 215, 175), HLS(33, 120, 76), 'DarkSeaGreen2', 151 +) +lightCyan3 = Colors.register( + RGB(175, 215, 215), HLS(33, 180, 76), 'LightCyan3', 152 +) +lightSkyBlue1 = Colors.register( + RGB(175, 215, 255), HLS(100, 210, 84), 'LightSkyBlue1', 153 +) +greenYellow = Colors.register( + RGB(175, 255, 0), HLS(100, 8, 50), 'GreenYellow', 154 +) +darkOliveGreen2 = Colors.register( + RGB(175, 255, 95), HLS(100, 90, 68), 'DarkOliveGreen2', 155 +) +paleGreen1 = Colors.register( + RGB(175, 255, 135), HLS(100, 100, 76), 'PaleGreen1', 156 +) +darkSeaGreen2 = Colors.register( + RGB(175, 255, 175), HLS(100, 120, 84), 'DarkSeaGreen2', 157 +) +darkSeaGreen1 = Colors.register( + RGB(175, 255, 215), HLS(100, 150, 84), 'DarkSeaGreen1', 158 +) +paleTurquoise1 = Colors.register( + RGB(175, 255, 255), HLS(100, 180, 84), 'PaleTurquoise1', 159 +) +red3 = Colors.register(RGB(215, 0, 0), HLS(100, 0, 42), 'Red3', 160) +deepPink3 = Colors.register( + RGB(215, 0, 95), HLS(100, 33, 42), 'DeepPink3', 161 +) +deepPink3 = Colors.register( + RGB(215, 0, 135), HLS(100, 22, 42), 'DeepPink3', 162 +) +magenta3 = Colors.register(RGB(215, 0, 175), HLS(100, 11, 42), 'Magenta3', 163) +magenta3 = Colors.register( + RGB(215, 0, 215), HLS(100, 300, 42), 'Magenta3', 164 +) +magenta2 = Colors.register(RGB(215, 0, 255), HLS(100, 90, 50), 'Magenta2', 165) +darkOrange3 = Colors.register( + RGB(215, 95, 0), HLS(100, 6, 42), 'DarkOrange3', 166 +) +indianRed = Colors.register(RGB(215, 95, 95), HLS(60, 0, 60), 'IndianRed', 167) +hotPink3 = Colors.register( + RGB(215, 95, 135), HLS(60, 340, 60), 'HotPink3', 168 +) +hotPink2 = Colors.register( + RGB(215, 95, 175), HLS(60, 320, 60), 'HotPink2', 169 +) +orchid = Colors.register(RGB(215, 95, 215), HLS(60, 300, 60), 'Orchid', 170) +mediumOrchid1 = Colors.register( + RGB(215, 95, 255), HLS(100, 285, 68), 'MediumOrchid1', 171 +) +orange3 = Colors.register(RGB(215, 135, 0), HLS(100, 7, 42), 'Orange3', 172) +lightSalmon3 = Colors.register( + RGB(215, 135, 95), HLS(60, 20, 60), 'LightSalmon3', 173 +) +lightPink3 = Colors.register( + RGB(215, 135, 135), HLS(50, 0, 68), 'LightPink3', 174 +) +pink3 = Colors.register(RGB(215, 135, 175), HLS(50, 330, 68), 'Pink3', 175) +plum3 = Colors.register(RGB(215, 135, 215), HLS(50, 300, 68), 'Plum3', 176) +violet = Colors.register(RGB(215, 135, 255), HLS(100, 280, 76), 'Violet', 177) +gold3 = Colors.register(RGB(215, 175, 0), HLS(100, 8, 42), 'Gold3', 178) +lightGoldenrod3 = Colors.register( + RGB(215, 175, 95), HLS(60, 40, 60), 'LightGoldenrod3', 179 +) +tan = Colors.register(RGB(215, 175, 135), HLS(50, 30, 68), 'Tan', 180) +mistyRose3 = Colors.register( + RGB(215, 175, 175), HLS(33, 0, 76), 'MistyRose3', 181 +) +thistle3 = Colors.register( + RGB(215, 175, 215), HLS(33, 300, 76), 'Thistle3', 182 +) +plum2 = Colors.register(RGB(215, 175, 255), HLS(100, 270, 84), 'Plum2', 183) +yellow3 = Colors.register(RGB(215, 215, 0), HLS(100, 60, 42), 'Yellow3', 184) +khaki3 = Colors.register(RGB(215, 215, 95), HLS(60, 60, 60), 'Khaki3', 185) +lightGoldenrod2 = Colors.register( + RGB(215, 215, 135), HLS(50, 60, 68), 'LightGoldenrod2', 186 +) +lightYellow3 = Colors.register( + RGB(215, 215, 175), HLS(33, 60, 76), 'LightYellow3', 187 +) +grey84 = Colors.register(RGB(215, 215, 215), HLS(0, 0, 84), 'Grey84', 188) +lightSteelBlue1 = Colors.register( + RGB(215, 215, 255), HLS(100, 240, 92), 'LightSteelBlue1', 189 +) +yellow2 = Colors.register(RGB(215, 255, 0), HLS(100, 9, 50), 'Yellow2', 190) +darkOliveGreen1 = Colors.register( + RGB(215, 255, 95), HLS(100, 75, 68), 'DarkOliveGreen1', 191 +) +darkOliveGreen1 = Colors.register( + RGB(215, 255, 135), HLS(100, 80, 76), 'DarkOliveGreen1', 192 +) +darkSeaGreen1 = Colors.register( + RGB(215, 255, 175), HLS(100, 90, 84), 'DarkSeaGreen1', 193 +) +honeydew2 = Colors.register( + RGB(215, 255, 215), HLS(100, 120, 92), 'Honeydew2', 194 +) +lightCyan1 = Colors.register( + RGB(215, 255, 255), HLS(100, 180, 92), 'LightCyan1', 195 +) +red1 = Colors.register(RGB(255, 0, 0), HLS(100, 0, 50), 'Red1', 196) +deepPink2 = Colors.register( + RGB(255, 0, 95), HLS(100, 37, 50), 'DeepPink2', 197 +) +deepPink1 = Colors.register( + RGB(255, 0, 135), HLS(100, 28, 50), 'DeepPink1', 198 +) +deepPink1 = Colors.register( + RGB(255, 0, 175), HLS(100, 18, 50), 'DeepPink1', 199 +) +magenta2 = Colors.register(RGB(255, 0, 215), HLS(100, 9, 50), 'Magenta2', 200) +magenta1 = Colors.register( + RGB(255, 0, 255), HLS(100, 300, 50), 'Magenta1', 201 +) +orangeRed1 = Colors.register( + RGB(255, 95, 0), HLS(100, 2, 50), 'OrangeRed1', 202 +) +indianRed1 = Colors.register( + RGB(255, 95, 95), HLS(100, 0, 68), 'IndianRed1', 203 +) +indianRed1 = Colors.register( + RGB(255, 95, 135), HLS(100, 345, 68), 'IndianRed1', 204 +) +hotPink = Colors.register(RGB(255, 95, 175), HLS(100, 330, 68), 'HotPink', 205) +hotPink = Colors.register(RGB(255, 95, 215), HLS(100, 315, 68), 'HotPink', 206) +mediumOrchid1 = Colors.register( + RGB(255, 95, 255), HLS(100, 300, 68), 'MediumOrchid1', 207 +) +darkOrange = Colors.register( + RGB(255, 135, 0), HLS(100, 1, 50), 'DarkOrange', 208 +) +salmon1 = Colors.register(RGB(255, 135, 95), HLS(100, 15, 68), 'Salmon1', 209) +lightCoral = Colors.register( + RGB(255, 135, 135), HLS(100, 0, 76), 'LightCoral', 210 +) +paleVioletRed1 = Colors.register( + RGB(255, 135, 175), HLS(100, 340, 76), 'PaleVioletRed1', 211 +) +orchid2 = Colors.register( + RGB(255, 135, 215), HLS(100, 320, 76), 'Orchid2', 212 +) +orchid1 = Colors.register( + RGB(255, 135, 255), HLS(100, 300, 76), 'Orchid1', 213 +) +orange1 = Colors.register(RGB(255, 175, 0), HLS(100, 1, 50), 'Orange1', 214) +sandyBrown = Colors.register( + RGB(255, 175, 95), HLS(100, 30, 68), 'SandyBrown', 215 +) +lightSalmon1 = Colors.register( + RGB(255, 175, 135), HLS(100, 20, 76), 'LightSalmon1', 216 +) +lightPink1 = Colors.register( + RGB(255, 175, 175), HLS(100, 0, 84), 'LightPink1', 217 +) +pink1 = Colors.register(RGB(255, 175, 215), HLS(100, 330, 84), 'Pink1', 218) +plum1 = Colors.register(RGB(255, 175, 255), HLS(100, 300, 84), 'Plum1', 219) +gold1 = Colors.register(RGB(255, 215, 0), HLS(100, 0, 50), 'Gold1', 220) +lightGoldenrod2 = Colors.register( + RGB(255, 215, 95), HLS(100, 45, 68), 'LightGoldenrod2', 221 +) +lightGoldenrod2 = Colors.register( + RGB(255, 215, 135), HLS(100, 40, 76), 'LightGoldenrod2', 222 +) +navajoWhite1 = Colors.register( + RGB(255, 215, 175), HLS(100, 30, 84), 'NavajoWhite1', 223 +) +mistyRose1 = Colors.register( + RGB(255, 215, 215), HLS(100, 0, 92), 'MistyRose1', 224 +) +thistle1 = Colors.register( + RGB(255, 215, 255), HLS(100, 300, 92), 'Thistle1', 225 +) +yellow1 = Colors.register(RGB(255, 255, 0), HLS(100, 60, 50), 'Yellow1', 226) +lightGoldenrod1 = Colors.register( + RGB(255, 255, 95), HLS(100, 60, 68), 'LightGoldenrod1', 227 +) +khaki1 = Colors.register(RGB(255, 255, 135), HLS(100, 60, 76), 'Khaki1', 228) +wheat1 = Colors.register(RGB(255, 255, 175), HLS(100, 60, 84), 'Wheat1', 229) +cornsilk1 = Colors.register( + RGB(255, 255, 215), HLS(100, 60, 92), 'Cornsilk1', 230 +) +grey100 = Colors.register(RGB(255, 255, 255), HLS(0, 0, 100), 'Grey100', 231) +grey3 = Colors.register(RGB(8, 8, 8), HLS(0, 0, 3), 'Grey3', 232) +grey7 = Colors.register(RGB(18, 18, 18), HLS(0, 0, 7), 'Grey7', 233) +grey11 = Colors.register(RGB(28, 28, 28), HLS(0, 0, 10), 'Grey11', 234) +grey15 = Colors.register(RGB(38, 38, 38), HLS(0, 0, 14), 'Grey15', 235) +grey19 = Colors.register(RGB(48, 48, 48), HLS(0, 0, 18), 'Grey19', 236) +grey23 = Colors.register(RGB(58, 58, 58), HLS(0, 0, 22), 'Grey23', 237) +grey27 = Colors.register(RGB(68, 68, 68), HLS(0, 0, 26), 'Grey27', 238) +grey30 = Colors.register(RGB(78, 78, 78), HLS(0, 0, 30), 'Grey30', 239) +grey35 = Colors.register(RGB(88, 88, 88), HLS(0, 0, 34), 'Grey35', 240) +grey39 = Colors.register(RGB(98, 98, 98), HLS(0, 0, 37), 'Grey39', 241) +grey42 = Colors.register(RGB(108, 108, 108), HLS(0, 0, 40), 'Grey42', 242) +grey46 = Colors.register(RGB(118, 118, 118), HLS(0, 0, 46), 'Grey46', 243) +grey50 = Colors.register(RGB(128, 128, 128), HLS(0, 0, 50), 'Grey50', 244) +grey54 = Colors.register(RGB(138, 138, 138), HLS(0, 0, 54), 'Grey54', 245) +grey58 = Colors.register(RGB(148, 148, 148), HLS(0, 0, 58), 'Grey58', 246) +grey62 = Colors.register(RGB(158, 158, 158), HLS(0, 0, 61), 'Grey62', 247) +grey66 = Colors.register(RGB(168, 168, 168), HLS(0, 0, 65), 'Grey66', 248) +grey70 = Colors.register(RGB(178, 178, 178), HLS(0, 0, 69), 'Grey70', 249) +grey74 = Colors.register(RGB(188, 188, 188), HLS(0, 0, 73), 'Grey74', 250) +grey78 = Colors.register(RGB(198, 198, 198), HLS(0, 0, 77), 'Grey78', 251) +grey82 = Colors.register(RGB(208, 208, 208), HLS(0, 0, 81), 'Grey82', 252) +grey85 = Colors.register(RGB(218, 218, 218), HLS(0, 0, 85), 'Grey85', 253) +grey89 = Colors.register(RGB(228, 228, 228), HLS(0, 0, 89), 'Grey89', 254) +grey93 = Colors.register(RGB(238, 238, 238), HLS(0, 0, 93), 'Grey93', 255) + +dark_gradient = ColorGradient( + red1, + orangeRed1, + darkOrange, + orange1, + yellow1, + yellow2, + greenYellow, + green1, +) +light_gradient = ColorGradient( + red1, + orangeRed1, + darkOrange, + orange1, + gold3, + darkOliveGreen3, + yellow4, + green3, +) +bg_gradient = ColorGradient(black) + +# Check if the background is light or dark. This is by no means a foolproof +# method, but there is no reliable way to detect this. +if os.environ.get('COLORFGBG', '15;0').split(';')[-1] == str(white.xterm): + # Light background + gradient = light_gradient + primary = black +else: + # Default, expect a dark background + gradient = dark_gradient + primary = white + +if __name__ == '__main__': + red = Colors.register(RGB(255, 128, 128)) + # red = Colors.register(RGB(255, 100, 100)) + from progressbar.terminal import base + + for i in base.ColorSupport: + base.color_support = i + print(i, red.fg('RED!'), red.bg('RED!'), red.underline('RED!')) diff --git a/progressbar/terminal/os_specific/__init__.py b/progressbar/terminal/os_specific/__init__.py new file mode 100644 index 00000000..4cff9feb --- /dev/null +++ b/progressbar/terminal/os_specific/__init__.py @@ -0,0 +1,22 @@ +import sys + +if sys.platform.startswith('win'): + from .windows import ( + getch as _getch, + set_console_mode as _set_console_mode, + reset_console_mode as _reset_console_mode, + ) + +else: + from .posix import getch as _getch + + def _reset_console_mode(): + pass + + def _set_console_mode(): + pass + + +getch = _getch +reset_console_mode = _reset_console_mode +set_console_mode = _set_console_mode diff --git a/progressbar/terminal/os_specific/posix.py b/progressbar/terminal/os_specific/posix.py new file mode 100644 index 00000000..e46fbdf0 --- /dev/null +++ b/progressbar/terminal/os_specific/posix.py @@ -0,0 +1,15 @@ +import sys +import tty +import termios + + +def getch(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + return ch diff --git a/progressbar/terminal/os_specific/windows.py b/progressbar/terminal/os_specific/windows.py new file mode 100644 index 00000000..c611d417 --- /dev/null +++ b/progressbar/terminal/os_specific/windows.py @@ -0,0 +1,127 @@ +import ctypes +from ctypes.wintypes import ( + DWORD as _DWORD, + HANDLE as _HANDLE, + BOOL as _BOOL, + WORD as _WORD, + UINT as _UINT, + WCHAR as _WCHAR, + CHAR as _CHAR, + SHORT as _SHORT, +) + +_kernel32 = ctypes.windll.Kernel32 + +_ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 +_ENABLE_PROCESSED_OUTPUT = 0x0001 +_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + +_STD_INPUT_HANDLE = _DWORD(-10) +_STD_OUTPUT_HANDLE = _DWORD(-11) + + +_GetConsoleMode = _kernel32.GetConsoleMode +_GetConsoleMode.restype = _BOOL + +_SetConsoleMode = _kernel32.SetConsoleMode +_SetConsoleMode.restype = _BOOL + +_GetStdHandle = _kernel32.GetStdHandle +_GetStdHandle.restype = _HANDLE + +_ReadConsoleInput = _kernel32.ReadConsoleInputA +_ReadConsoleInput.restype = _BOOL + + +_hConsoleInput = _GetStdHandle(_STD_INPUT_HANDLE) +_input_mode = _DWORD() +_GetConsoleMode(_HANDLE(_hConsoleInput), ctypes.byref(_input_mode)) + +_hConsoleOutput = _GetStdHandle(_STD_OUTPUT_HANDLE) +_output_mode = _DWORD() +_GetConsoleMode(_HANDLE(_hConsoleOutput), ctypes.byref(_output_mode)) + + +class _COORD(ctypes.Structure): + _fields_ = [('X', _SHORT), ('Y', _SHORT)] + + +class _FOCUS_EVENT_RECORD(ctypes.Structure): + _fields_ = [('bSetFocus', _BOOL)] + + +class _KEY_EVENT_RECORD(ctypes.Structure): + class _uchar(ctypes.Union): + _fields_ = [('UnicodeChar', _WCHAR), ('AsciiChar', _CHAR)] + + _fields_ = [ + ('bKeyDown', _BOOL), + ('wRepeatCount', _WORD), + ('wVirtualKeyCode', _WORD), + ('wVirtualScanCode', _WORD), + ('uChar', _uchar), + ('dwControlKeyState', _DWORD), + ] + + +class _MENU_EVENT_RECORD(ctypes.Structure): + _fields_ = [('dwCommandId', _UINT)] + + +class _MOUSE_EVENT_RECORD(ctypes.Structure): + _fields_ = [ + ('dwMousePosition', _COORD), + ('dwButtonState', _DWORD), + ('dwControlKeyState', _DWORD), + ('dwEventFlags', _DWORD), + ] + + +class _WINDOW_BUFFER_SIZE_RECORD(ctypes.Structure): + _fields_ = [('dwSize', _COORD)] + + +class _INPUT_RECORD(ctypes.Structure): + class _Event(ctypes.Union): + _fields_ = [ + ('KeyEvent', _KEY_EVENT_RECORD), + ('MouseEvent', _MOUSE_EVENT_RECORD), + ('WindowBufferSizeEvent', _WINDOW_BUFFER_SIZE_RECORD), + ('MenuEvent', _MENU_EVENT_RECORD), + ('FocusEvent', _FOCUS_EVENT_RECORD), + ] + + _fields_ = [('EventType', _WORD), ('Event', _Event)] + + +def reset_console_mode(): + _SetConsoleMode(_HANDLE(_hConsoleInput), _DWORD(_input_mode.value)) + _SetConsoleMode(_HANDLE(_hConsoleOutput), _DWORD(_output_mode.value)) + + +def set_console_mode(): + mode = _input_mode.value | _ENABLE_VIRTUAL_TERMINAL_INPUT + _SetConsoleMode(_HANDLE(_hConsoleInput), _DWORD(mode)) + + mode = ( + _output_mode.value + | _ENABLE_PROCESSED_OUTPUT + | _ENABLE_VIRTUAL_TERMINAL_PROCESSING + ) + _SetConsoleMode(_HANDLE(_hConsoleOutput), _DWORD(mode)) + + +def getch(): + lpBuffer = (_INPUT_RECORD * 2)() + nLength = _DWORD(2) + lpNumberOfEventsRead = _DWORD() + + _ReadConsoleInput( + _HANDLE(_hConsoleInput), + lpBuffer, + nLength, + ctypes.byref(lpNumberOfEventsRead), + ) + + char = lpBuffer[1].Event.KeyEvent.uChar.AsciiChar.decode('ascii') + return None if char == '\x00' else char diff --git a/progressbar/terminal/stream.py b/progressbar/terminal/stream.py new file mode 100644 index 00000000..b4c3f233 --- /dev/null +++ b/progressbar/terminal/stream.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import sys +from types import TracebackType +from typing import Iterable, Iterator, Type + +from progressbar import base + + +class TextIOOutputWrapper(base.TextIO): + def __init__(self, stream: base.TextIO): + self.stream = stream + + def close(self) -> None: + self.stream.close() + + def fileno(self) -> int: + return self.stream.fileno() + + def flush(self) -> None: + pass + + def isatty(self) -> bool: + return self.stream.isatty() + + def read(self, __n: int = -1) -> str: + return self.stream.read(__n) + + def readable(self) -> bool: + return self.stream.readable() + + def readline(self, __limit: int = -1) -> str: + return self.stream.readline(__limit) + + def readlines(self, __hint: int = ...) -> list[str]: + return self.stream.readlines(__hint) + + def seek(self, __offset: int, __whence: int = ...) -> int: + return self.stream.seek(__offset, __whence) + + def seekable(self) -> bool: + return self.stream.seekable() + + def tell(self) -> int: + return self.stream.tell() + + def truncate(self, __size: int | None = ...) -> int: + return self.stream.truncate(__size) + + def writable(self) -> bool: + return self.stream.writable() + + def writelines(self, __lines: Iterable[str]) -> None: + return self.stream.writelines(__lines) + + def __next__(self) -> str: + return self.stream.__next__() + + def __iter__(self) -> Iterator[str]: + return self.stream.__iter__() + + def __exit__( + self, + __t: Type[BaseException] | None, + __value: BaseException | None, + __traceback: TracebackType | None, + ) -> None: + return self.stream.__exit__(__t, __value, __traceback) + + def __enter__(self) -> base.TextIO: + return self.stream.__enter__() + + +class LineOffsetStreamWrapper(TextIOOutputWrapper): + UP = '\033[F' + DOWN = '\033[B' + + def __init__(self, lines=0, stream=sys.stderr): + self.lines = lines + super().__init__(stream) + + def write(self, data): + # Move the cursor up + self.stream.write(self.UP * self.lines) + # Print a carriage return to reset the cursor position + self.stream.write('\r') + # Print the data without newlines so we don't change the position + self.stream.write(data.rstrip('\n')) + # Move the cursor down + self.stream.write(self.DOWN * self.lines) + + self.flush() + + +class LastLineStream(TextIOOutputWrapper): + line: str = '' + + def seekable(self) -> bool: + return False + + def readable(self) -> bool: + return True + + def read(self, __n: int = -1) -> str: + return self.line[:__n] + + def readline(self, __limit: int = -1) -> str: + return self.line[:__limit] + + def write(self, data): + self.line = data + + def truncate(self, __size: int | None = None) -> int: + self.line = '' if __size is None else self.line[:__size] + return len(self.line) + + def writelines(self, __lines: Iterable[str]) -> None: + line = '' + # Walk through the lines and take the last one + for line in __lines: + pass + + self.line = line diff --git a/progressbar/utils.py b/progressbar/utils.py index 7f84a91d..d1d0d00b 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -39,7 +39,7 @@ 'tmux', 'vt(10[02]|220|320)', ) -ANSI_TERM_RE = re.compile('^({})'.format('|'.join(ANSI_TERMS)), re.IGNORECASE) +ANSI_TERM_RE = re.compile(f"^({'|'.join(ANSI_TERMS)})", re.IGNORECASE) def is_ansi_terminal( @@ -157,8 +157,10 @@ def no_color(value: StringT) -> StringT: if isinstance(value, bytes): pattern: bytes = '\\\u001b\\[.*?[@-~]'.encode() return re.sub(pattern, b'', value) # type: ignore - else: + elif isinstance(value, str): return re.sub(u'\x1b\\[.*?[@-~]', '', value) # type: ignore + else: + raise TypeError('`value` must be a string or bytes, got %r' % value) def len_color(value: types.StringTypes) -> int: @@ -229,8 +231,7 @@ def flush(self) -> None: self.buffer.flush() def _flush(self) -> None: - value = self.buffer.getvalue() - if value: + if value := self.buffer.getvalue(): self.flush() self.target.write(value) self.buffer.seek(0) @@ -436,27 +437,25 @@ def needs_clear(self) -> bool: # pragma: no cover return stderr_needs_clear or stdout_needs_clear def flush(self) -> None: - if self.wrapped_stdout: # pragma: no branch - if isinstance(self.stdout, WrappingIO): # pragma: no branch - try: - self.stdout._flush() - except io.UnsupportedOperation: # pragma: no cover - self.wrapped_stdout = False - logger.warning( - 'Disabling stdout redirection, %r is not seekable', - sys.stdout, - ) - - if self.wrapped_stderr: # pragma: no branch - if isinstance(self.stderr, WrappingIO): # pragma: no branch - try: - self.stderr._flush() - except io.UnsupportedOperation: # pragma: no cover - self.wrapped_stderr = False - logger.warning( - 'Disabling stderr redirection, %r is not seekable', - sys.stderr, - ) + if self.wrapped_stdout and isinstance(self.stdout, WrappingIO): + try: + self.stdout._flush() + except io.UnsupportedOperation: # pragma: no cover + self.wrapped_stdout = False + logger.warning( + 'Disabling stdout redirection, %r is not seekable', + sys.stdout, + ) + + if self.wrapped_stderr and isinstance(self.stderr, WrappingIO): + try: + self.stderr._flush() + except io.UnsupportedOperation: # pragma: no cover + self.wrapped_stderr = False + logger.warning( + 'Disabling stderr redirection, %r is not seekable', + sys.stderr, + ) def excepthook(self, exc_type, exc_value, exc_traceback): self.original_excepthook(exc_type, exc_value, exc_traceback) @@ -513,7 +512,7 @@ def __getattr__(self, name: str) -> int: if name in self: return self[name] else: - raise AttributeError("No such attribute: " + name) + raise AttributeError(f"No such attribute: {name}") def __setattr__(self, name: str, value: int) -> None: self[name] = value @@ -522,7 +521,7 @@ def __delattr__(self, name: str) -> None: if name in self: del self[name] else: - raise AttributeError("No such attribute: " + name) + raise AttributeError(f"No such attribute: {name}") logger = logging.getLogger(__name__) diff --git a/progressbar/widgets.py b/progressbar/widgets.py index b13d9f33..199e3fab 100644 --- a/progressbar/widgets.py +++ b/progressbar/widgets.py @@ -7,10 +7,12 @@ import pprint import sys import typing +from typing import Callable from python_utils import converters, types -from . import base, utils +from . import base, terminal, utils +from .terminal import colors if types.TYPE_CHECKING: from .bar import ProgressBarMixinBase @@ -136,10 +138,7 @@ def __call__( '''Formats the widget into a string''' format = self.get_format(progress, data, format) try: - if self.new_style: - return format.format(**data) - else: - return format % data + return format.format(**data) if self.new_style else format % data except (TypeError, KeyError): print('Error while formatting %r' % format, file=sys.stderr) pprint.pprint(data, stream=sys.stderr) @@ -222,6 +221,43 @@ def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str: progress - a reference to the calling ProgressBar ''' + _fixed_colors: dict[str, terminal.Color | None] = dict() + _gradient_colors: dict[str, terminal.OptionalColor | None] = dict() + _len: Callable[[str | bytes], int] = len + + @functools.cached_property + def uses_colors(self): + for key, value in self._gradient_colors.items(): + if value is not None: + return True + + return any(value is not None for key, value in self._fixed_colors.items()) + + def _apply_colors(self, text: str, data: Data) -> str: + if self.uses_colors: + return terminal.apply_colors( + text, + data.get('percentage'), + **self._gradient_colors, + **self._fixed_colors, + ) + else: + return text + + def __init__( + self, *args, fixed_colors=None, gradient_colors=None, **kwargs + ): + if fixed_colors is not None: + self._fixed_colors.update(fixed_colors) + + if gradient_colors is not None: + self._gradient_colors.update(gradient_colors) + + if self.uses_colors: + self._len = utils.len_color + + super().__init__(*args, **kwargs) + class AutoWidthWidgetBase(WidgetBase, metaclass=abc.ABCMeta): '''The base class for all variable width widgets. @@ -289,10 +325,7 @@ def __call__( ): for name, (key, transform) in self.mapping.items(): try: - if transform is None: - data[name] = data[key] - else: - data[name] = transform(data[key]) + data[name] = data[key] if transform is None else transform(data[key]) except (KeyError, ValueError, IndexError): # pragma: no cover pass @@ -358,10 +391,10 @@ def __init__( TimeSensitiveWidgetBase.__init__(self, **kwargs) def get_sample_times(self, progress: ProgressBarMixinBase, data: Data): - return progress.extra.setdefault(self.key_prefix + 'sample_times', []) + return progress.extra.setdefault(f'{self.key_prefix}sample_times', []) def get_sample_values(self, progress: ProgressBarMixinBase, data: Data): - return progress.extra.setdefault(self.key_prefix + 'sample_values', []) + return progress.extra.setdefault(f'{self.key_prefix}sample_values', []) def __call__( self, progress: ProgressBarMixinBase, data: Data, delta: bool = False @@ -369,11 +402,7 @@ def __call__( sample_times = self.get_sample_times(progress, data) sample_values = self.get_sample_values(progress, data) - if sample_times: - sample_time = sample_times[-1] - else: - sample_time = datetime.datetime.min - + sample_time = sample_times[-1] if sample_times else datetime.datetime.min if progress.last_update_time - sample_time > self.INTERVAL: # Add a sample but limit the size to `num_samples` sample_times.append(progress.last_update_time) @@ -389,20 +418,15 @@ def __call__( ): sample_times.pop(0) sample_values.pop(0) - else: - if len(sample_times) > self.samples: - sample_times.pop(0) - sample_values.pop(0) + elif len(sample_times) > self.samples: + sample_times.pop(0) + sample_values.pop(0) - if delta: - delta_time = sample_times[-1] - sample_times[0] - delta_value = sample_values[-1] - sample_values[0] - if delta_time: - return delta_time, delta_value - else: - return None, None - else: + if not delta: return sample_times, sample_values + delta_time = sample_times[-1] - sample_times[0] + delta_value = sample_values[-1] - sample_values[0] + return (delta_time, delta_value) if delta_time else (None, None) class ETA(Timer): @@ -417,7 +441,6 @@ def __init__( format_NA='ETA: N/A', **kwargs, ): - if '%s' in format and '%(eta)s' not in format: format = format.replace('%s', '%(eta)s') @@ -432,15 +455,12 @@ def _calculate_eta( self, progress: ProgressBarMixinBase, data: Data, value, elapsed ): '''Updates the widget to show the ETA or total time when finished.''' - if elapsed: - # The max() prevents zero division errors - per_item = elapsed.total_seconds() / max(value, 1e-6) - remaining = progress.max_value - data['value'] - eta_seconds = remaining * per_item - else: - eta_seconds = 0 + if not elapsed: + return 0 - return eta_seconds + # The max() prevents zero division errors + per_item = elapsed.total_seconds() / max(value, 1e-6) + return (progress.max_value - data['value']) * per_item def __call__( self, @@ -737,7 +757,18 @@ def __call__( return FormatWidgetMixin.__call__(self, progress, data, format) -class Percentage(FormatWidgetMixin, WidgetBase): +class ColoredMixin: + _fixed_colors: dict[str, terminal.Color | None] = dict( + fg_none=colors.yellow, + bg_none=None, + ) + _gradient_colors: dict[str, terminal.OptionalColor | None] = dict( + fg=colors.gradient, + bg=None, + ) + + +class Percentage(FormatWidgetMixin, ColoredMixin, WidgetBase): '''Displays the current percentage as a number with a percent sign.''' def __init__(self, format='%(percentage)3d%%', na='N/A%%', **kwargs): @@ -751,12 +782,14 @@ def get_format( # If percentage is not available, display N/A% percentage = data.get('percentage', base.Undefined) if not percentage and percentage != 0: - return self.na + output = self.na + else: + output = FormatWidgetMixin.get_format(self, progress, data, format) - return FormatWidgetMixin.get_format(self, progress, data, format) + return self._apply_colors(output, data) -class SimpleProgress(FormatWidgetMixin, WidgetBase): +class SimpleProgress(FormatWidgetMixin, ColoredMixin, WidgetBase): '''Returns progress as a count of the total (e.g.: "5 of 47")''' max_width_cache: dict[ @@ -769,24 +802,15 @@ class SimpleProgress(FormatWidgetMixin, WidgetBase): def __init__(self, format=DEFAULT_FORMAT, **kwargs): FormatWidgetMixin.__init__(self, format=format, **kwargs) WidgetBase.__init__(self, format=format, **kwargs) - self.max_width_cache = dict() - self.max_width_cache['default'] = self.max_width or 0 + self.max_width_cache = {'default': self.max_width or 0} def __call__( self, progress: ProgressBarMixinBase, data: Data, format=None ): # If max_value is not available, display N/A - if data.get('max_value'): - data['max_value_s'] = data.get('max_value') - else: - data['max_value_s'] = 'N/A' - + data['max_value_s'] = data['max_value'] if data.get('max_value') else 'N/A' # if value is not available it's the zeroth iteration - if data.get('value'): - data['value_s'] = data['value'] - else: - data['value_s'] = 0 - + data['value_s'] = data['value'] if data.get('value') else 0 formatted = FormatWidgetMixin.__call__( self, progress, data, format=format ) @@ -803,12 +827,11 @@ def __call__( continue temporary_data['value'] = value - width = progress.custom_len( + if width := progress.custom_len( FormatWidgetMixin.__call__( self, progress, temporary_data, format=format ) - ) - if width: # pragma: no branch + ): max_width = max(max_width or 0, width) self.max_width_cache[key] = max_width @@ -817,12 +840,15 @@ def __call__( if max_width: # pragma: no branch formatted = formatted.rjust(max_width) - return formatted + return self._apply_colors(formatted, data) class Bar(AutoWidthWidgetBase): '''A progress bar which stretches to fill the line.''' + fg: terminal.OptionalColor | None = colors.gradient + bg: terminal.OptionalColor | None = None + def __init__( self, marker='#', @@ -857,6 +883,7 @@ def __call__( progress: ProgressBarMixinBase, data: Data, width: int = 0, + color=True, ): '''Updates the progress bar and its subcomponents''' @@ -874,6 +901,9 @@ def __call__( else: marker = marker.rjust(width, fill) + if color: + marker = self._apply_colors(marker, data) + return left + marker + right @@ -1069,10 +1099,7 @@ def get_values(self, progress: ProgressBarMixinBase, data: Data): value = float(progress_value) / float(progress_max) if not 0 <= value <= 1: - raise ValueError( - 'Range value needs to be in the range [0..1], got %s' - % value - ) + raise ValueError(f'Range value needs to be in the range [0..1], got {value}') range_ = value * (len(ranges) - 1) pos = int(range_) @@ -1158,8 +1185,7 @@ def __call__( marker = self.markers[-1] * int(num_chars) - marker_idx = int((num_chars % 1) * (len(self.markers) - 1)) - if marker_idx: + if marker_idx := int((num_chars % 1) * (len(self.markers) - 1)): marker += self.markers[marker_idx] marker = converters.to_unicode(marker) @@ -1186,13 +1212,27 @@ def __call__( # type: ignore format: FormatString = None, ): center = FormatLabel.__call__(self, progress, data, format=format) - bar = Bar.__call__(self, progress, data, width) + bar = Bar.__call__(self, progress, data, width, color=False) # Aligns the center of the label to the center of the bar center_len = progress.custom_len(center) center_left = int((width - center_len) / 2) center_right = center_left + center_len - return bar[:center_left] + center + bar[center_right:] + + return ( + self._apply_colors( + bar[:center_left], + data, + ) + + self._apply_colors( + center, + data, + ) + + self._apply_colors( + bar[center_right:], + data, + ) + ) class PercentageLabelBar(Percentage, FormatLabelBar): diff --git a/pytest.ini b/pytest.ini index bdfd4dec..d6a47d53 100644 --- a/pytest.ini +++ b/pytest.ini @@ -19,6 +19,7 @@ norecursedirs = dist .ropeproject .tox + progressbar/terminal/os_specific filterwarnings = ignore::DeprecationWarning diff --git a/setup.py b/setup.py index 850df2de..ab559f5c 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ install_reqs = [] if sys.argv[-1] == 'info': for k, v in about.items(): - print('%s: %s' % (k, v)) + print(f'{k}: {v}') sys.exit() if os.path.isfile('README.rst'): diff --git a/tests/original_examples.py b/tests/original_examples.py index 2e521e9d..f85c1311 100644 --- a/tests/original_examples.py +++ b/tests/original_examples.py @@ -45,14 +45,18 @@ def example1(): @example def example2(): + + + class CrazyFileTransferSpeed(FileTransferSpeed): """It's bigger between 45 and 80 percent.""" def update(self, pbar): if 45 < pbar.percentage() < 80: - return 'Bigger Now ' + FileTransferSpeed.update(self,pbar) + return f'Bigger Now {FileTransferSpeed.update(self, pbar)}' else: return FileTransferSpeed.update(self,pbar) + widgets = [CrazyFileTransferSpeed(),' <<<', Bar(), '>>> ', Percentage(),' ', ETA()] pbar = ProgressBar(widgets=widgets, maxval=10000) @@ -103,40 +107,40 @@ def example6(): @example def example7(): pbar = ProgressBar() # Progressbar can guess maxval automatically. - for i in pbar(range(80)): + for _ in pbar(range(80)): time.sleep(0.01) @example def example8(): pbar = ProgressBar(maxval=80) # Progressbar can't guess maxval. - for i in pbar((i for i in range(80))): + for _ in pbar(iter(range(80))): time.sleep(0.01) @example def example9(): pbar = ProgressBar(widgets=['Working: ', AnimatedMarker()]) - for i in pbar((i for i in range(50))): + for _ in pbar(iter(range(50))): time.sleep(.08) @example def example10(): widgets = ['Processed: ', Counter(), ' lines (', Timer(), ')'] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(150))): + for _ in pbar(iter(range(150))): time.sleep(0.1) @example def example11(): widgets = [FormatLabel('Processed: %(value)d lines (in: %(elapsed)s)')] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(150))): + for _ in pbar(iter(range(150))): time.sleep(0.1) @example def example12(): widgets = ['Balloon: ', AnimatedMarker(markers='.oO@* ')] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(24))): + for _ in pbar(iter(range(24))): time.sleep(0.3) @example @@ -145,7 +149,7 @@ def example13(): try: widgets = ['Arrows: ', AnimatedMarker(markers='←↖↑↗→↘↓↙')] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(24))): + for _ in pbar(iter(range(24))): time.sleep(0.3) except UnicodeError: sys.stdout.write('Unicode error: skipping example') @@ -155,7 +159,7 @@ def example14(): try: widgets = ['Arrows: ', AnimatedMarker(markers='◢◣◤◥')] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(24))): + for _ in pbar(iter(range(24))): time.sleep(0.3) except UnicodeError: sys.stdout.write('Unicode error: skipping example') @@ -165,7 +169,7 @@ def example15(): try: widgets = ['Wheels: ', AnimatedMarker(markers='◐◓◑◒')] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(24))): + for _ in pbar(iter(range(24))): time.sleep(0.3) except UnicodeError: sys.stdout.write('Unicode error: skipping example') @@ -173,7 +177,7 @@ def example15(): def example16(): widgets = [FormatLabel('Bouncer: value %(value)d - '), BouncingBar()] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(180))): + for _ in pbar(iter(range(180))): time.sleep(0.05) @example @@ -182,7 +186,7 @@ def example17(): BouncingBar(marker=RotatingMarker())] pbar = ProgressBar(widgets=widgets) - for i in pbar((i for i in range(180))): + for _ in pbar(iter(range(180))): time.sleep(0.05) @example @@ -200,10 +204,10 @@ def example18(): @example def example19(): - pbar = ProgressBar() - for i in pbar([]): - pass - pbar.finish() + pbar = ProgressBar() + for _ in pbar([]): + pass + pbar.finish() @example def example20(): diff --git a/tests/test_color.py b/tests/test_color.py new file mode 100644 index 00000000..e1b2c487 --- /dev/null +++ b/tests/test_color.py @@ -0,0 +1,42 @@ +import pytest + +import progressbar +from progressbar import terminal + + +@pytest.mark.parametrize( + 'variable', [ + 'PROGRESSBAR_ENABLE_COLORS', + 'FORCE_COLOR', + ] +) +def test_color_environment_variables(monkeypatch, variable): + monkeypatch.setattr( + terminal, + 'color_support', + terminal.ColorSupport.XTERM_256, + ) + + monkeypatch.setenv(variable, '1') + bar = progressbar.ProgressBar() + assert bar.enable_colors + + monkeypatch.setenv(variable, '0') + bar = progressbar.ProgressBar() + assert not bar.enable_colors + + +def test_enable_colors_flags(): + bar = progressbar.ProgressBar(enable_colors=True) + assert bar.enable_colors + + bar = progressbar.ProgressBar(enable_colors=False) + assert not bar.enable_colors + + bar = progressbar.ProgressBar( + enable_colors=terminal.ColorSupport.XTERM_TRUECOLOR + ) + assert bar.enable_colors + + with pytest.raises(ValueError): + progressbar.ProgressBar(enable_colors=12345) diff --git a/tests/test_custom_widgets.py b/tests/test_custom_widgets.py index 95252818..22b1e66b 100644 --- a/tests/test_custom_widgets.py +++ b/tests/test_custom_widgets.py @@ -1,4 +1,7 @@ import time + +import pytest + import progressbar @@ -7,8 +10,7 @@ class CrazyFileTransferSpeed(progressbar.FileTransferSpeed): def update(self, pbar): if 45 < pbar.percentage() < 80: - return 'Bigger Now ' + progressbar.FileTransferSpeed.update(self, - pbar) + return f'Bigger Now {progressbar.FileTransferSpeed.update(self, pbar)}' else: return progressbar.FileTransferSpeed.update(self, pbar) @@ -60,6 +62,8 @@ def test_variable_widget_widget(): p.update(i, text=False) i += 1 p.update(i, text=True, error='a') + with pytest.raises(TypeError): + p.update(i, non_existing_variable='error!') p.finish() diff --git a/tests/test_end.py b/tests/test_end.py index 75d45723..42842a2e 100644 --- a/tests/test_end.py +++ b/tests/test_end.py @@ -37,7 +37,7 @@ def test_end_100(monkeypatch): max_value=103, ) - for x in range(0, 102): + for x in range(102): p.update(x) data = p.data() diff --git a/tests/test_failure.py b/tests/test_failure.py index 030ab292..204053c8 100644 --- a/tests/test_failure.py +++ b/tests/test_failure.py @@ -64,7 +64,7 @@ def test_one_max_value(): def test_changing_max_value(): '''Changing max_value? No problem''' p = progressbar.ProgressBar(max_value=10)(range(20), max_value=20) - for i in p: + for _ in p: time.sleep(1) @@ -122,3 +122,15 @@ def test_variable_not_str(): def test_variable_too_many_strs(): with pytest.raises(ValueError): progressbar.Variable('too long') + + +def test_negative_value(): + bar = progressbar.ProgressBar(max_value=10) + with pytest.raises(ValueError): + bar.update(value=-1) + + +def test_increment(): + bar = progressbar.ProgressBar(max_value=10) + bar.increment() + del bar diff --git a/tests/test_flush.py b/tests/test_flush.py index 69dc4e30..f6336d8d 100644 --- a/tests/test_flush.py +++ b/tests/test_flush.py @@ -5,6 +5,7 @@ def test_flush(): '''Left justify using the terminal width''' p = progressbar.ProgressBar(poll_interval=0.001) + p.print('hello') for i in range(10): print('pre-updates', p.updates) diff --git a/tests/test_iterators.py b/tests/test_iterators.py index b32c529e..218f048c 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -6,14 +6,14 @@ def test_list(): '''Progressbar can guess max_value automatically.''' p = progressbar.ProgressBar() - for i in p(range(10)): + for _ in p(range(10)): time.sleep(0.001) def test_iterator_with_max_value(): '''Progressbar can't guess max_value.''' p = progressbar.ProgressBar(max_value=10) - for i in p((i for i in range(10))): + for _ in p(iter(range(10))): time.sleep(0.001) @@ -21,7 +21,7 @@ def test_iterator_without_max_value_error(): '''Progressbar can't guess max_value.''' p = progressbar.ProgressBar() - for i in p((i for i in range(10))): + for _ in p(iter(range(10))): time.sleep(0.001) assert p.max_value is progressbar.UnknownLength @@ -35,7 +35,7 @@ def test_iterator_without_max_value(): progressbar.BouncingBar(), progressbar.BouncingBar(marker=progressbar.RotatingMarker()), ]) - for i in p((i for i in range(10))): + for _ in p(iter(range(10))): time.sleep(0.001) @@ -43,7 +43,7 @@ def test_iterator_with_incorrect_max_value(): '''Progressbar can't guess max_value.''' p = progressbar.ProgressBar(max_value=10) with pytest.raises(ValueError): - for i in p((i for i in range(20))): + for _ in p(iter(range(20))): time.sleep(0.001) diff --git a/tests/test_monitor_progress.py b/tests/test_monitor_progress.py index 5dd6f5ee..f1e843db 100644 --- a/tests/test_monitor_progress.py +++ b/tests/test_monitor_progress.py @@ -90,12 +90,10 @@ def test_generator_example(testdir): result.stderr.lines = [l for l in result.stderr.lines if l.strip()] pprint.pprint(result.stderr.lines, width=70) - lines = [] - for i in range(9): - lines.append( - r'[/\\|\-]\s+\|\s*#\s*\| %(i)d Elapsed Time: \d:00:%(i)02d' % - dict(i=i)) - + lines = [ + r'[/\\|\-]\s+\|\s*#\s*\| %(i)d Elapsed Time: \d:00:%(i)02d' % dict(i=i) + for i in range(9) + ] result.stderr.re_match_lines(lines) diff --git a/tests/test_multibar.py b/tests/test_multibar.py index fe1c569f..11c5f3e8 100644 --- a/tests/test_multibar.py +++ b/tests/test_multibar.py @@ -1,4 +1,8 @@ +import threading +import time + import pytest + import progressbar @@ -18,3 +22,83 @@ def test_multi_progress_bar_out_of_range(): def test_multi_progress_bar_fill_left(): import examples return examples.multi_progress_bar_example(False) + + +def test_multibar(): + bars = 3 + N = 10 + multibar = progressbar.MultiBar(sort_keyfunc=lambda bar: bar.label) + multibar.start() + multibar.append_label = False + multibar.prepend_label = True + + # Test handling of progressbars that don't call the super constructors + bar = progressbar.ProgressBar(max_value=N) + bar.index = -1 + multibar['x'] = bar + bar.start() + # Test twice for other code paths + multibar['x'] = bar + multibar._label_bar(bar) + multibar._label_bar(bar) + bar.finish() + del multibar['x'] + + multibar.append_label = True + + def do_something(bar): + for j in bar(range(N)): + time.sleep(0.01) + bar.update(j) + + for i in range(bars): + thread = threading.Thread( + target=do_something, + args=(multibar['bar {}'.format(i)],) + ) + thread.start() + + for bar in multibar.values(): + for j in range(N): + bar.update(j) + time.sleep(0.002) + + multibar.join(0.1) + multibar.stop(0.1) + + +@pytest.mark.parametrize( + 'sort_key', [ + None, + 'index', + 'label', + 'value', + 'percentage', + progressbar.SortKey.CREATED, + progressbar.SortKey.LABEL, + progressbar.SortKey.VALUE, + progressbar.SortKey.PERCENTAGE, + ] +) +def test_multibar_sorting(sort_key): + bars = 3 + N = 10 + + with progressbar.MultiBar() as multibar: + for i in range(bars): + label = f'bar {i}' + multibar[label] = progressbar.ProgressBar(max_value=N) + + for bar in multibar.values(): + for _ in bar(range(N)): + assert bar.started() + time.sleep(0.002) + + for bar in multibar.values(): + assert bar.finished() + + +def test_offset_bar(): + with progressbar.ProgressBar(line_offset=2) as bar: + for i in range(100): + bar.update(i) diff --git a/tests/test_progressbar.py b/tests/test_progressbar.py index 32083eb0..3e20ab63 100644 --- a/tests/test_progressbar.py +++ b/tests/test_progressbar.py @@ -63,6 +63,9 @@ def test_dirty(): bar = progressbar.ProgressBar() bar.start() + assert bar.started() for i in range(10): bar.update(i) bar.finish(dirty=True) + assert bar.finished() + assert bar.started() diff --git a/tests/test_stream.py b/tests/test_stream.py index 6dcfcf7c..abb9f48c 100644 --- a/tests/test_stream.py +++ b/tests/test_stream.py @@ -6,7 +6,7 @@ def test_nowrap(): # Make sure we definitely unwrap - for i in range(5): + for _ in range(5): progressbar.streams.unwrap(stderr=True, stdout=True) stdout = sys.stdout @@ -23,13 +23,13 @@ def test_nowrap(): assert stderr == sys.stderr # Make sure we definitely unwrap - for i in range(5): + for _ in range(5): progressbar.streams.unwrap(stderr=True, stdout=True) def test_wrap(): # Make sure we definitely unwrap - for i in range(5): + for _ in range(5): progressbar.streams.unwrap(stderr=True, stdout=True) stdout = sys.stdout @@ -50,7 +50,7 @@ def test_wrap(): assert stderr == sys.stderr # Make sure we definitely unwrap - for i in range(5): + for _ in range(5): progressbar.streams.unwrap(stderr=True, stdout=True) diff --git a/tests/test_timed.py b/tests/test_timed.py index 6753f537..b7c3170b 100644 --- a/tests/test_timed.py +++ b/tests/test_timed.py @@ -57,7 +57,7 @@ def test_adaptive_eta(): ) p.start() - for i in range(20): + for _ in range(20): p.update(1) time.sleep(0.001) p.finish() @@ -160,8 +160,7 @@ def test_eta_not_available(): ETAs should not raise exceptions. """ def gen(): - for x in range(200): - yield x + yield from range(200) widgets = [progressbar.AdaptiveETA(), progressbar.ETA()] diff --git a/tests/test_unicode.py b/tests/test_unicode.py index 3b8e4aec..7ef6bc83 100644 --- a/tests/test_unicode.py +++ b/tests/test_unicode.py @@ -19,11 +19,11 @@ def test_markers(name, markers, as_unicode): markers = converters.to_str(markers) widgets = [ - '%s: ' % name.capitalize(), + f'{name.capitalize()}: ', progressbar.AnimatedMarker(markers=markers), ] bar = progressbar.ProgressBar(widgets=widgets) bar._MINIMUM_UPDATE_INTERVAL = 1e-12 - for i in bar((i for i in range(24))): + for _ in bar(iter(range(24))): time.sleep(0.001) diff --git a/tests/test_unknown_length.py b/tests/test_unknown_length.py index fe08e209..8b2cbdfc 100644 --- a/tests/test_unknown_length.py +++ b/tests/test_unknown_length.py @@ -25,4 +25,4 @@ def test_unknown_length_at_start(): pb2 = progressbar.ProgressBar().start(max_value=progressbar.UnknownLength) for w in pb2.widgets: print(type(w), repr(w)) - assert any([isinstance(w, progressbar.Bar) for w in pb2.widgets]) + assert any(isinstance(w, progressbar.Bar) for w in pb2.widgets) diff --git a/tests/test_widgets.py b/tests/test_widgets.py index a38574da..23044f23 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -57,12 +57,12 @@ def test_widgets_large_values(max_value): def test_format_widget(): - widgets = [] - for mapping in progressbar.FormatLabel.mapping: - widgets.append(progressbar.FormatLabel('%%(%s)r' % mapping)) - + widgets = [ + progressbar.FormatLabel('%%(%s)r' % mapping) + for mapping in progressbar.FormatLabel.mapping + ] p = progressbar.ProgressBar(widgets=widgets) - for i in p(range(10)): + for _ in p(range(10)): time.sleep(1) diff --git a/tox.ini b/tox.ini index 99be8934..3ffed050 100644 --- a/tox.ini +++ b/tox.ini @@ -42,6 +42,9 @@ commands = black --skip-string-normalization --line-length 79 {toxinidir}/progre changedir = basepython = python3 deps = -r{toxinidir}/docs/requirements.txt +allowlist_externals = + rm + mkdir whitelist_externals = rm cd @@ -51,8 +54,7 @@ commands = mkdir -p docs/_static sphinx-apidoc -e -o docs/ progressbar rm -f docs/modules.rst - rm -f docs/progressbar.rst - sphinx-build -W -b html -d docs/_build/doctrees docs docs/_build/html {posargs} + sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html {posargs} [flake8] ignore = W391, W504, E741, W503, E131