Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/major-version-backlog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Major-version backlog

Breaking changes that the quality audit identified as worth making but that
are **intentionally deferred** to the next major version, because fixing them
now would change the public API or long-standing rendered/CLI behaviour that
users may depend on. Each item is kept working as-is in the current release
line; this document records *why* it is deferred and *where* the code lives so
the change is easy to pick up later.

Nothing here is a bug that affects correctness of the documented API today —
those were fixed in the audit PRs. These are design debts whose fix is a
compatibility break.

## MultiBar composes over `dict` instead of subclassing it

`MultiBar` subclasses `dict` (`progressbar/multi.py`,
`class MultiBar(dict[str, bar.ProgressBar])`). The subclassing leaks several
surprising behaviours: an auto-vivifying `__getitem__` that creates a bar on
missing-key access, state that is dual-keyed by both label and bar object, and
`print` redirection installed by monkeypatching (`bar.print` / `self.print`).
Composition over inheritance (holding a private mapping and exposing an
explicit, documented interface) would remove the auto-vivification foot-gun and
clarify the threading/rendering API, but it changes the type's identity and the
mapping methods callers can use, so it must wait for a major version.

## `FormatLabelBar.__call__` diamond dispatch and incompatible override

`FormatLabelBar` (`progressbar/widgets.py`) manually dispatches to both parents
via `FormatLabel.__call__(self, ...)` and `Bar.__call__(self, ...)`, and its
`__call__` override carries a `# type: ignore` because its signature is not
compatible with the base `WidgetBase.__call__` contract. A CodeQL alert on the
incompatible override was dismissed as by-design: the diamond is deliberate and
the widths line up at runtime. Making the signatures genuinely compatible (or
folding the composition into a cooperative call chain) is a public-signature
change, so it is deferred.

## CLI no-op `pv`-compatibility flags

`progressbar/__main__.py` declares a `pv(1)`-compatible flag set, but only a
few (`--buffer-size`, `--eta`, `--input`/positional, `--line-mode`, `--size`)
actually feed the runtime. The rest — `--timer`, `--rate`, `--numeric`,
`--delay-start`, `--interval`, `--height`, `--width`, and friends — are parsed
and then silently ignored. They exist so `pv`-style command lines do not error
out. The next major version should either wire each flag to real behaviour or
reject unsupported flags explicitly; both are user-visible CLI changes, so they
are deferred.

## `__next__` / `next` manual-iteration path

`ProgressBar.__next__` (`progressbar/bar.py`) advances the bar on manual
iteration but bypasses the fast redraw gate that `update()` goes through, so
hand-rolled `next(bar)` loops render on a different schedule than the normal
paths. `next = __next__` (same file) is a Python-2-era alias kept so old code
calling `bar.next()` keeps working. Routing `__next__` through the gate and
dropping the `next` alias are behaviour/API changes, so they are deferred.

## `ColorBase` and `WindowsColor` no-op public classes

`ColorBase` (`progressbar/terminal/base.py`) is an abstract base that its own
docstring describes as deprecated (it only exists because `typing.NamedTuple`
cannot be used as a base for the real `Color`). `WindowsColor` (same file) is
effectively a no-op that duplicates `DummyColor`: recent Windows terminals
support ANSI, so it passes text through unstyled. Both are importable public
names, so removing them is a compatibility break for the next major version.

## Unused `Colors` lookup indexes

`Colors.register` (`progressbar/terminal/base.py`) populates four reverse
indexes on every registration — `by_name`, `by_lowername`, `by_hex`, and
`by_hls` — but nothing in the tree ever reads them. For the 256-colour table
that is 256×4 list appends plus the backing dicts at import time, for lookups
no code performs. Dropping the unused indexes would cut import work and memory,
but they are public class attributes, so their removal is deferred.

## 16-colour terminals receive the `38;5;N` SGR form

For a detected 16-colour terminal (`env.ColorSupport.XTERM`), `Color.ansi`
(`progressbar/terminal/base.py`) still emits the indexed `38;5;N` / `48;5;N`
256-colour SGR form (with `N` derived from the 16-colour palette) rather than
the canonical `30`–`37` / `90`–`97` direct codes. This is a pre-existing
convention that real terminals tolerate; switching to the direct codes changes
the exact bytes emitted for every colour on those terminals, so it is deferred.

## Deferred `DeprecationWarning` upgrades (silent-compat policy)

Several backwards-compatibility shims currently accept legacy usage silently
rather than warning:

- `apply_colors(**kwargs)` (`progressbar/terminal/base.py`) swallows unknown
keyword arguments instead of rejecting them.
- Legacy aliases such as `RotatingMarker` (for `AnimatedMarker`) and
`DynamicMessage` (for `Variable`) in `progressbar/widgets.py`.
- The `%s` → `%(elapsed)s` / `%(eta)s` format shims in `Timer` / `ETA`
(`progressbar/widgets.py`).

Emitting `DeprecationWarning` from these paths is the right long-term move, but
warnings are observable behaviour (and can break `-W error` test suites), so
the upgrade is a coordinated major-version change.

## `deltas_to_seconds` sentinel/`ValueError` contract

`deltas_to_seconds` (`progressbar/utils.py`) is now expressed as typed
overloads, but its runtime contract around the not-a-number / default sentinel
still leans on raising `ValueError` for a class of inputs. A cleaner redesign
(an explicit sentinel type or an `Optional`-returning overload set) would be a
signature/behaviour change for callers that catch `ValueError`, so it is
deferred to the API redesign in the next major version.
6 changes: 4 additions & 2 deletions progressbar/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
)
__email__ = 'wolph@wol.ph'
__version__ = '4.5.0'
#: Release date of ``__version__`` as a static string; bump on each release.
__date__ = '2024-08-29'
__license__ = 'BSD'
__copyright__ = 'Copyright 2015 Rick van Hattem (Wolph)'
__url__ = 'https://github.com/WoLpH/python-progressbar'
__copyright__ = 'Copyright 2015-2026 Rick van Hattem (Wolph)'
__url__ = 'https://github.com/wolph/python-progressbar'
11 changes: 8 additions & 3 deletions progressbar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@

import importlib
import typing
from datetime import date

from .__about__ import __author__, __version__
# Re-exported as a static module attribute (kept out of ``__all__`` for
# backwards compatibility). ``__date__`` used to be recomputed at import time
# via ``date.today()``; it now mirrors the release date from ``__about__``.
from .__about__ import (
__author__,
__date__ as __date__,
__version__,
)

if typing.TYPE_CHECKING:
# Eager imports for type checkers only; loaded lazily at runtime by
Expand Down Expand Up @@ -140,7 +146,6 @@ def __dir__() -> list[str]:
return sorted(set(globals()) | set(__all__) | _SUBMODULES)


__date__ = str(date.today())
#: Canonical export list, kept equal to ``sorted(_NAME_TO_MODULE)`` (the single
#: source of truth for lazily re-exported names) plus the eagerly imported
#: dunders. Held as a static literal so type checkers can see the re-exports;
Expand Down
10 changes: 4 additions & 6 deletions progressbar/bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,9 @@ class ProgressBarMixinBase(abc.ABC):
#: The default keyword arguments for the `default_widgets` if no widgets
#: are configured
widget_kwargs: dict[str, typing.Any]
#: Custom length function for multibyte characters such as CJK
# mypy and pyright can't agree on what the correct one is... so we'll
# need to use a helper function :(
# custom_len: types.Callable[['ProgressBarMixinBase', str], int]
#: Custom length function for multibyte characters such as CJK. A plain
#: ``Callable[[str], int]`` is used (rather than a bound-method signature)
#: because mypy and pyright disagree on the more precise form.
custom_len: collections.abc.Callable[[str], int]
#: The time the progress bar was started
initial_start_time: datetime | None
Expand Down Expand Up @@ -207,6 +206,7 @@ def __repr__(self):
label = f': {self.label}' if self.label else ''
return f'<{self.__class__.__name__}#{self.index}{label}>'


class DefaultFdMixin(ProgressBarMixinBase):
# The file descriptor to write to. Defaults to `sys.stderr`
fd: base.TextIO = sys.stderr
Expand Down Expand Up @@ -933,8 +933,6 @@ def data(self) -> dict[str, typing.Any]:
:py:meth:`_mark_update` before the widgets read them.
"""
elapsed = self.last_update_time - self.start_time # type: ignore
# For Python 2.7 and higher we have _`timedelta.total_seconds`, but we
# want to support older versions as well
total_seconds_elapsed = utils.deltas_to_seconds(elapsed)
return dict(
# The maximum value (can be None with iterators)
Expand Down
6 changes: 0 additions & 6 deletions progressbar/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,6 @@ def is_terminal(
return is_terminal


# Enable Windows full color mode if possible
if os.name == 'nt':
pass

# os_specific.set_console_mode()

JUPYTER = bool(
os.environ.get('JUPYTER_COLUMNS')
or os.environ.get('JUPYTER_LINES')
Expand Down
44 changes: 5 additions & 39 deletions progressbar/terminal/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import collections.abc
import colorsys
import enum
import threading
import typing
from collections import defaultdict

Expand All @@ -18,7 +17,11 @@
base as pbase,
env,
)
from .os_specific import getch

# Re-exported for backwards compatibility (previously consumed by the removed
# ``_CPR`` cursor-position helper; guarded by the API snapshot). The redundant
# alias marks the re-export as intentional so it is not stripped as unused.
from .os_specific import getch as getch

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'getch' is not used.
Comment thread
wolph marked this conversation as resolved.
Dismissed

ESC = '\x1b'

Expand Down Expand Up @@ -145,43 +148,6 @@
return UP(n) + CLEAR_LINE_ALL() + DOWN(n)


# Report Cursor Position (CPR), response = [row;column] as row;columnR
class _CPR(str): # pragma: no cover # pyright: ignore[reportUnusedClass]
_response_lock = threading.Lock()

def __call__(self, stream: typing.IO[str]) -> tuple[int, int]:
res: str = ''

with self._response_lock:
stream.write(str(self))
stream.flush()

while not res.endswith('R'):
char = getch()

if char:
res += char

res_list = res[2:-1].split(';')

res_list = tuple(
int(item) if item.isdigit() else item for item in res_list
)

if len(res_list) == 1:
return typing.cast(tuple[int, int], res_list[0])

return typing.cast(tuple[int, int], tuple(res_list))

def row(self, stream: typing.IO[str]) -> int:
row, _ = self(stream)
return row

def column(self, stream: typing.IO[str]) -> int:
_, column = self(stream)
return column


class WindowsColors(enum.Enum):
BLACK = 0, 0, 0
BLUE = 0, 0, 128
Expand Down
Loading
Loading