From 837be153e13e70e23ea5bb340d750b659732cc52 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 9 Sep 2026 01:12:25 +0200 Subject: [PATCH 01/18] fix: point deprecation warnings at the caller The maxval, poll, and currval deprecation warnings used stacklevel=1, so they named progressbar's own source instead of the line that used the old name. A fixed stacklevel=2 would only be right for currval: _apply_deprecated_aliases is reached through ProgressBar.__init__ and any subclass __init__ chain above it, so the depth varies per caller. _caller_stacklevel() walks the frames from the warning site outward until the code is no longer inside the progressbar package and returns that depth. Five tests record the warning and assert its filename and line are the caller's, for a plain bar, a DataTransferBar, a user subclass with its own __init__, and the currval property. --- progressbar/bar.py | 28 ++++++++- tests/test_deprecation_warnings.py | 95 ++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/test_deprecation_warnings.py diff --git a/progressbar/bar.py b/progressbar/bar.py index 3cb8d963..6bf2d268 100644 --- a/progressbar/bar.py +++ b/progressbar/bar.py @@ -41,6 +41,28 @@ ) from .terminal import os_specific +_PACKAGE_DIR: str = os.path.dirname(os.path.abspath(__file__)) + + +def _caller_stacklevel() -> int: + """Return the `stacklevel` that points a warning at the first frame + outside progressbar. + + `warnings.warn()` counts level 1 as the function that calls it, so the + walk starts at this helper's caller and climbs until the code is not + ours. A fixed level would break as soon as a subclass adds an + `__init__` between the user and `ProgressBar.__init__`. + """ + frame: FrameType | None = sys._getframe(1) + level: int = 1 + while frame is not None and frame.f_code.co_filename.startswith( + _PACKAGE_DIR + ): + frame = frame.f_back + level += 1 + return level + + try: # Optional native accelerator, shipped as the ``progressbar2[fast]`` extra # (the separate ``speedups`` package). When importable, the iterator path @@ -904,7 +926,7 @@ def _apply_deprecated_aliases( 'The usage of `maxval` is deprecated, please use ' '`max_value` instead', DeprecationWarning, - stacklevel=1, + stacklevel=_caller_stacklevel(), ) max_value = kwargs.get('maxval') @@ -913,7 +935,7 @@ def _apply_deprecated_aliases( 'The usage of `poll` is deprecated, please use ' '`poll_interval` instead', DeprecationWarning, - stacklevel=1, + stacklevel=_caller_stacklevel(), ) poll_interval = kwargs.get('poll') @@ -1738,7 +1760,7 @@ def currval(self) -> NumberT: warnings.warn( 'The usage of `currval` is deprecated, please use `value` instead', DeprecationWarning, - stacklevel=1, + stacklevel=_caller_stacklevel(), ) return self.value diff --git a/tests/test_deprecation_warnings.py b/tests/test_deprecation_warnings.py new file mode 100644 index 00000000..9dfcdb5d --- /dev/null +++ b/tests/test_deprecation_warnings.py @@ -0,0 +1,95 @@ +"""The deprecation warnings must point at the caller, not at progressbar. + +A warning attributed to `progressbar/bar.py` tells the user nothing about +which of their own lines used the deprecated name. Each test below records +the warning and checks that it lands in this file, on the line that used +`maxval`, `poll` or `currval`, whatever the depth of the `__init__` chain +in between. +""" + +from __future__ import annotations + +import sys +import typing +import warnings + +import progressbar + + +def _line_before() -> int: + """Return the line number of the statement above the calling line.""" + return sys._getframe(1).f_lineno - 1 + + +def _single_deprecation( + caught: list[warnings.WarningMessage], +) -> warnings.WarningMessage: + assert len(caught) == 1 + assert caught[0].category is DeprecationWarning + return caught[0] + + +class _BarWithOwnInit(progressbar.ProgressBar): + """A user subclass that adds one frame between the caller and the bar.""" + + super_call_line: int + + def __init__(self, **kwargs: typing.Any) -> None: + super().__init__(**kwargs) + self.super_call_line = _line_before() + + +def test_maxval_warning_points_at_caller() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always', DeprecationWarning) + progressbar.ProgressBar(maxval=10) + expected_line: int = _line_before() + + warning: warnings.WarningMessage = _single_deprecation(caught) + assert warning.filename == __file__ + assert warning.lineno == expected_line + + +def test_poll_warning_points_at_caller() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always', DeprecationWarning) + progressbar.ProgressBar(poll=1) + expected_line: int = _line_before() + + warning: warnings.WarningMessage = _single_deprecation(caught) + assert warning.filename == __file__ + assert warning.lineno == expected_line + + +def test_currval_warning_points_at_caller() -> None: + bar: progressbar.ProgressBar = progressbar.ProgressBar(max_value=10) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always', DeprecationWarning) + assert bar.currval == 0 + expected_line: int = _line_before() + + warning: warnings.WarningMessage = _single_deprecation(caught) + assert warning.filename == __file__ + assert warning.lineno == expected_line + + +def test_subclass_maxval_warning_points_at_caller() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always', DeprecationWarning) + progressbar.DataTransferBar(maxval=10) + expected_line: int = _line_before() + + warning: warnings.WarningMessage = _single_deprecation(caught) + assert warning.filename == __file__ + assert warning.lineno == expected_line + + +def test_user_subclass_init_warning_points_at_super_call() -> None: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always', DeprecationWarning) + bar: _BarWithOwnInit = _BarWithOwnInit(maxval=10) + + warning: warnings.WarningMessage = _single_deprecation(caught) + assert warning.filename == __file__ + assert warning.lineno == bar.super_call_line From 276b462be3895329759b377bdf3a5e363a6b44e7 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 13 Sep 2026 06:22:48 +0200 Subject: [PATCH 02/18] Refresh documentation homepage with recorded demos and coloured cards --- docs/_ext/demo.py | 15 +- docs/_static/brand.css | 41 ++++++ docs/_static/demos/tutorial-step1.svg | 2 +- docs/_static/home.css | 127 +++++++++++++++++ docs/_static/livecode/livecode.js | 11 +- docs/_static/showcase.js | 124 +++++++++++++++++ docs/_templates/page.html | 64 +++++++++ docs/conf.py | 20 ++- docs/examples/tutorial/step1.py | 7 +- docs/index.rst | 121 ++++++++++++---- progressbar/utils.py | 12 +- tests/console/test_homepage.py | 193 ++++++++++++++++++++++++++ tests/console/test_showcase_assets.py | 38 +++++ 13 files changed, 734 insertions(+), 41 deletions(-) create mode 100644 docs/_static/brand.css create mode 100644 docs/_static/home.css create mode 100644 docs/_static/showcase.js create mode 100644 docs/_templates/page.html create mode 100644 tests/console/test_homepage.py create mode 100644 tests/console/test_showcase_assets.py diff --git a/docs/_ext/demo.py b/docs/_ext/demo.py index 59e32fb0..5647f179 100644 --- a/docs/_ext/demo.py +++ b/docs/_ext/demo.py @@ -17,7 +17,7 @@ from docutils import nodes from docutils.parsers.rst import Directive from sphinx.application import Sphinx -from sphinx.errors import NoUri +from sphinx.errors import ExtensionError, NoUri from sphinx.util.osutil import relative_uri if typing.TYPE_CHECKING: @@ -155,8 +155,21 @@ def copy_example_sources(app: Sphinx, exception: Exception | None) -> None: ) +def validate_showcase_assets(_app: Sphinx) -> None: + """Check recordings embedded as raw HTML on the documentation homepage.""" + name: str + for name in ('readme/colors', 'readme/multibar', 'readme/hero'): + svg_path: pathlib.Path = DEMOS_BY_NAME[name].svg_path + if not svg_path.is_file(): + raise ExtensionError( + f'showcase animation not rendered: {name} ' + f'(run: python scripts/render_demos.py --only {name})' + ) + + def setup(app: Sphinx) -> dict[str, typing.Any]: app.add_directive('demo', DemoDirective) + app.connect('builder-inited', validate_showcase_assets) app.connect('build-finished', copy_example_sources) app.add_css_file('vendor/xterm.css') app.add_css_file('livecode/livecode.css') diff --git a/docs/_static/brand.css b/docs/_static/brand.css new file mode 100644 index 00000000..782e22ac --- /dev/null +++ b/docs/_static/brand.css @@ -0,0 +1,41 @@ +/* Shared typography and controls for the homepage and Furo documentation. */ +body { + --home-accent: #6550b5; + --home-card-transfer: #e1f0ee; + --home-card-jobs: #eee5f7; + --home-card-logs: #f8edcf; + --home-surface: #efece5; +} +body[data-theme="dark"] { + --home-accent: #b9a5f0; + --home-card-transfer: #203a37; + --home-card-jobs: #352c44; + --home-card-logs: #403922; + --home-surface: #28272b; +} +@media (prefers-color-scheme: dark) { + body:not([data-theme="light"]) { + --home-accent: #b9a5f0; + --home-card-transfer: #203a37; + --home-card-jobs: #352c44; + --home-card-logs: #403922; + --home-surface: #28272b; + } +} +a { text-underline-offset: 0.18em; } +a:focus-visible, button:focus-visible, input:focus-visible, +textarea:focus-visible, [tabindex]:focus-visible { + outline: 3px solid #b87917; + outline-offset: 4px; +} +.sidebar-brand-text { font-weight: 750; letter-spacing: -0.035em; } +.demo-button { background: #6550b5; color: #fff; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/docs/_static/demos/tutorial-step1.svg b/docs/_static/demos/tutorial-step1.svg index ece0d3b6..36821d0c 100644 --- a/docs/_static/demos/tutorial-step1.svg +++ b/docs/_static/demos/tutorial-step1.svg @@ -7,7 +7,7 @@ viewBox="0 0 1080 96" > Wrap an iterable - The first progress bar: wrap a range with progressbar.progressbar. + Wrap an iterable to show its progress. + + + + + Three workers reporting task progress + Total 0 of 3 files | |sales.csv 0% | |users.csv 0% | |Total 0 of 3 files | |stock.csv 0% | |sales.csv 2% | |users.csv 1% | |Total 0 of 3 files | |stock.csv 1% | |sales.csv 2% | |users.csv 3% |# |Total 0 of 3 files | |stock.csv 2% | |sales.csv 3% |# |users.csv 4% |# |Total 0 of 3 files | |stock.csv 4% |# |sales.csv 4% |# |users.csv 4% |# |Total 0 of 3 files | |stock.csv 5% |## |sales.csv 5% |## |users.csv 5% |## |Total 0 of 3 files | |stock.csv 6% |## |sales.csv 5% |## |users.csv 6% |## |Total 0 of 3 files | |stock.csv 7% |## |sales.csv 6% |## |users.csv 6% |## |Total 0 of 3 files | |stock.csv 8% |### |sales.csv 7% |## |users.csv 7% |## |Total 0 of 3 files | |stock.csv 8% |### |sales.csv 8% |### |users.csv 8% |### |Total 0 of 3 files | |stock.csv 9% |### |sales.csv 8% |### |users.csv 10% |#### |Total 0 of 3 files | |stock.csv 10% |#### |sales.csv 9% |### |users.csv 10% |#### |Total 0 of 3 files | |stock.csv 11% |#### |sales.csv 10% |#### |users.csv 12% |#### |Total 0 of 3 files | |stock.csv 11% |#### |sales.csv 11% |#### |users.csv 13% |##### |Total 0 of 3 files | |stock.csv 12% |#### |sales.csv 12% |#### |users.csv 14% |##### |Total 0 of 3 files | |stock.csv 13% |##### |sales.csv 12% |#### |users.csv 15% |###### |Total 0 of 3 files | |stock.csv 13% |##### |sales.csv 13% |##### |users.csv 16% |###### |Total 0 of 3 files | |stock.csv 14% |##### |sales.csv 14% |##### |users.csv 17% |###### |Total 0 of 3 files | |stock.csv 15% |###### |sales.csv 14% |##### |users.csv 18% |####### |Total 0 of 3 files | |stock.csv 15% |###### |sales.csv 16% |###### |users.csv 19% |####### |Total 0 of 3 files | |stock.csv 16% |###### |sales.csv 16% |###### |users.csv 20% |######## |Total 0 of 3 files | |stock.csv 17% |###### |sales.csv 18% |####### |users.csv 20% |######## |Total 0 of 3 files | |stock.csv 18% |####### |sales.csv 18% |####### |users.csv 21% |######## |Total 0 of 3 files | |stock.csv 18% |####### |sales.csv 19% |####### |users.csv 22% |######### |Total 0 of 3 files | |stock.csv 19% |####### |sales.csv 20% |######## |users.csv 23% |######### |Total 0 of 3 files | |stock.csv 20% |######## |sales.csv 21% |######## |users.csv 23% |######### |Total 0 of 3 files | |stock.csv 21% |######## |sales.csv 21% |######## |users.csv 25% |########## |Total 0 of 3 files | |stock.csv 22% |######### |sales.csv 22% |######### |users.csv 25% |########## |Total 0 of 3 files | |stock.csv 22% |######### |sales.csv 24% |######### |users.csv 26% |########## |Total 0 of 3 files | |stock.csv 24% |######### |sales.csv 24% |######### |users.csv 28% |########### |Total 0 of 3 files | |stock.csv 24% |######### |sales.csv 26% |########## |users.csv 29% |########### |Total 0 of 3 files | |stock.csv 26% |########## |sales.csv 26% |########## |users.csv 29% |########### |Total 0 of 3 files | |stock.csv 26% |########## |sales.csv 27% |########### |users.csv 30% |############ |Total 0 of 3 files | |stock.csv 27% |########### |sales.csv 28% |########### |users.csv 31% |############ |Total 0 of 3 files | |stock.csv 27% |########### |sales.csv 29% |########### |users.csv 33% |############# |Total 0 of 3 files | |stock.csv 28% |########### |sales.csv 30% |############ |users.csv 34% |############# |Total 0 of 3 files | |stock.csv 29% |########### |sales.csv 30% |############ |users.csv 35% |############## |Total 0 of 3 files | |stock.csv 30% |############ |sales.csv 31% |############ |users.csv 36% |############## |Total 0 of 3 files | |stock.csv 30% |############ |sales.csv 32% |############# |users.csv 37% |############### |Total 0 of 3 files | |stock.csv 31% |############ |sales.csv 33% |############# |users.csv 37% |############### |Total 0 of 3 files | |stock.csv 33% |############# |sales.csv 33% |############# |users.csv 39% |############### |Total 0 of 3 files | |stock.csv 33% |############# |sales.csv 34% |############# |users.csv 41% |################ |Total 0 of 3 files | |stock.csv 34% |############# |sales.csv 35% |############## |users.csv 42% |################# |Total 0 of 3 files | |stock.csv 35% |############## |sales.csv 36% |############## |users.csv 42% |################# |Total 0 of 3 files | |stock.csv 36% |############## |sales.csv 37% |############### |users.csv 43% |################# |Total 0 of 3 files | |stock.csv 36% |############## |sales.csv 38% |############### |users.csv 44% |################## |Total 0 of 3 files | |stock.csv 37% |############### |sales.csv 39% |############### |users.csv 45% |################## |Total 0 of 3 files | |stock.csv 38% |############### |sales.csv 39% |############### |users.csv 46% |################## |Total 0 of 3 files | |stock.csv 38% |############### |sales.csv 40% |################ |users.csv 47% |################### |Total 0 of 3 files | |stock.csv 39% |############### |sales.csv 41% |################ |users.csv 48% |################### |Total 0 of 3 files | |stock.csv 40% |################ |sales.csv 41% |################ |users.csv 49% |#################### |Total 0 of 3 files | |stock.csv 41% |################ |sales.csv 42% |################# |users.csv 50% |#################### |Total 0 of 3 files | |stock.csv 42% |################# |sales.csv 43% |################# |users.csv 50% |#################### |Total 0 of 3 files | |stock.csv 43% |################# |sales.csv 44% |################## |users.csv 51% |#################### |Total 0 of 3 files | |stock.csv 44% |################## |sales.csv 45% |################## |users.csv 51% |#################### |Total 0 of 3 files | |stock.csv 44% |################## |sales.csv 46% |################## |users.csv 52% |##################### |Total 0 of 3 files | |stock.csv 45% |################## |sales.csv 47% |################### |users.csv 53% |##################### |Total 0 of 3 files | |stock.csv 46% |################## |sales.csv 47% |################### |users.csv 55% |###################### |Total 0 of 3 files | |stock.csv 47% |################### |sales.csv 48% |################### |users.csv 56% |###################### |Total 0 of 3 files | |stock.csv 48% |################### |sales.csv 50% |#################### |users.csv 56% |###################### |Total 0 of 3 files | |stock.csv 49% |#################### |sales.csv 50% |#################### |users.csv 58% |####################### |Total 0 of 3 files | |stock.csv 50% |#################### |sales.csv 51% |#################### |users.csv 58% |####################### |Total 0 of 3 files | |stock.csv 51% |#################### |sales.csv 52% |##################### |users.csv 59% |######################## |Total 0 of 3 files | |stock.csv 51% |#################### |sales.csv 53% |##################### |users.csv 60% |######################## |Total 0 of 3 files | |stock.csv 52% |##################### |sales.csv 53% |##################### |users.csv 61% |######################### |Total 0 of 3 files | |stock.csv 53% |##################### |sales.csv 54% |###################### |users.csv 62% |######################### |Total 0 of 3 files | |stock.csv 53% |##################### |sales.csv 55% |###################### |users.csv 63% |######################### |Total 0 of 3 files | |stock.csv 54% |###################### |sales.csv 56% |###################### |users.csv 64% |########################## |Total 0 of 3 files | |stock.csv 55% |###################### |sales.csv 56% |###################### |users.csv 65% |########################## |Total 0 of 3 files | |stock.csv 56% |###################### |sales.csv 57% |####################### |users.csv 66% |########################### |Total 0 of 3 files | |stock.csv 57% |####################### |sales.csv 58% |####################### |users.csv 66% |########################### |Total 0 of 3 files | |stock.csv 57% |####################### |sales.csv 59% |######################## |users.csv 68% |########################### |Total 0 of 3 files | |stock.csv 58% |####################### |sales.csv 60% |######################## |users.csv 69% |############################ |Total 0 of 3 files | |stock.csv 59% |######################## |sales.csv 60% |######################## |users.csv 70% |############################ |Total 0 of 3 files | |stock.csv 60% |######################## |sales.csv 61% |######################### |users.csv 71% |############################# |Total 0 of 3 files | |stock.csv 60% |######################## |sales.csv 62% |######################### |users.csv 72% |############################# |Total 0 of 3 files | |stock.csv 62% |######################### |sales.csv 63% |######################### |users.csv 73% |############################# |Total 0 of 3 files | |stock.csv 62% |######################### |sales.csv 64% |########################## |users.csv 74% |############################## |Total 0 of 3 files | |stock.csv 63% |######################### |sales.csv 65% |########################## |users.csv 75% |############################## |Total 0 of 3 files | |stock.csv 64% |########################## |sales.csv 65% |########################## |users.csv 76% |############################### |Total 0 of 3 files | |stock.csv 65% |########################## |sales.csv 66% |########################### |users.csv 76% |############################### |Total 0 of 3 files | |stock.csv 65% |########################## |sales.csv 68% |########################### |users.csv 77% |############################### |Total 0 of 3 files | |stock.csv 66% |########################### |sales.csv 68% |########################### |users.csv 78% |############################### |Total 0 of 3 files | |stock.csv 67% |########################### |sales.csv 69% |############################ |users.csv 79% |################################ |Total 0 of 3 files | |stock.csv 68% |########################### |sales.csv 69% |############################ |users.csv 81% |################################# |Total 0 of 3 files | |stock.csv 69% |############################ |sales.csv 71% |############################# |users.csv 81% |################################# |Total 0 of 3 files | |stock.csv 70% |############################ |sales.csv 71% |############################# |users.csv 83% |################################## |Total 0 of 3 files | |stock.csv 70% |############################ |sales.csv 72% |############################# |users.csv 84% |################################## |Total 0 of 3 files | |stock.csv 72% |############################# |sales.csv 73% |############################# |users.csv 84% |################################## |Total 0 of 3 files | |stock.csv 72% |############################# |sales.csv 74% |############################## |users.csv 85% |################################## |Total 0 of 3 files | |stock.csv 73% |############################# |sales.csv 75% |############################## |users.csv 86% |################################### |Total 0 of 3 files | |stock.csv 74% |############################## |sales.csv 75% |############################## |users.csv 87% |################################### |Total 0 of 3 files | |stock.csv 75% |############################## |sales.csv 76% |############################### |users.csv 88% |#################################### |Total 0 of 3 files | |stock.csv 76% |############################### |sales.csv 77% |############################### |users.csv 88% |#################################### |Total 0 of 3 files | |stock.csv 76% |############################### |sales.csv 78% |############################### |users.csv 90% |#################################### |Total 0 of 3 files | |stock.csv 77% |############################### |sales.csv 79% |################################ |users.csv 90% |#################################### |Total 0 of 3 files | |stock.csv 78% |############################### |sales.csv 79% |################################ |users.csv 91% |##################################### |Total 0 of 3 files | |stock.csv 78% |############################### |sales.csv 81% |################################# |users.csv 92% |##################################### |Total 0 of 3 files | |stock.csv 79% |################################ |sales.csv 81% |################################# |users.csv 93% |###################################### |Total 0 of 3 files | |stock.csv 80% |################################ |sales.csv 82% |################################# |users.csv 94% |###################################### |Total 0 of 3 files | |stock.csv 81% |################################# |sales.csv 83% |################################## |users.csv 94% |###################################### |Total 0 of 3 files | |stock.csv 82% |################################# |sales.csv 84% |################################## |users.csv 95% |###################################### |Total 0 of 3 files | |stock.csv 83% |################################## |sales.csv 84% |################################## |users.csv 96% |####################################### |Total 0 of 3 files | |stock.csv 83% |################################## |sales.csv 85% |################################## |users.csv 97% |####################################### |Total 0 of 3 files | |stock.csv 84% |################################## |sales.csv 86% |################################### |users.csv 98% |######################################## |Total 0 of 3 files | |stock.csv 84% |################################## |sales.csv 87% |################################### |users.csv 99% |######################################## |Total 0 of 3 files | |stock.csv 85% |################################## |sales.csv 87% |################################### |sales.csv 88% |#################################### |Total 1 of 3 files |########### |stock.csv 85% |################################## |sales.csv 88% |#################################### |Total 1 of 3 files |########### |stock.csv 86% |################################### |sales.csv 91% |##################################### |Total 1 of 3 files |########### |stock.csv 87% |################################### |sales.csv 92% |##################################### |Total 1 of 3 files |########### |stock.csv 89% |#################################### |sales.csv 93% |###################################### |Total 1 of 3 files |########### |stock.csv 90% |#################################### |sales.csv 94% |###################################### |Total 1 of 3 files |########### |stock.csv 91% |##################################### |sales.csv 96% |####################################### |Total 1 of 3 files |########### |stock.csv 92% |##################################### |sales.csv 98% |######################################## |Total 1 of 3 files |########### |stock.csv 93% |###################################### |sales.csv 99% |######################################## |Total 1 of 3 files |########### |stock.csv 94% |###################################### |Total 2 of 3 files |###################### |stock.csv 96% |####################################### |Total 2 of 3 files |###################### |stock.csv 99% |######################################## |Total 2 of 3 files |###################### |Total 3 of 3 files |#################################| + diff --git a/docs/_static/demos/howto-prefix-suffix.svg b/docs/_static/demos/howto-prefix-suffix.svg index 2c116967..a10d6f76 100644 --- a/docs/_static/demos/howto-prefix-suffix.svg +++ b/docs/_static/demos/howto-prefix-suffix.svg @@ -1,13 +1,13 @@ - Templated prefix and suffix - Template prefix=/suffix= with the bar's own values, not a fixed string. + Live file and block labels + Show the current file and processed blocks in prefix/suffix templates. diff --git a/docs/_static/livecode/livecode.js b/docs/_static/livecode/livecode.js index 93eb4f12..15a4985a 100644 --- a/docs/_static/livecode/livecode.js +++ b/docs/_static/livecode/livecode.js @@ -178,12 +178,16 @@ function createPanel(container, source) { return panel; } -// Two demos cannot run under Pyodide at all: MultiBar starts a background +// Threaded demos cannot run under Pyodide: MultiBar starts a background // thread and `Thread.start()` raises `RuntimeError: can't start new thread` // there, rendering nothing first. The worker returns a friendly message // rather than a traceback, but the Run button should not be offered in the // first place -- the message is defence in depth, not the control. -const NON_RUNNABLE_DEMOS = new Set(['readme/multibar', 'howto/multibar']); +const NON_RUNNABLE_DEMOS = new Set([ + 'readme/multibar', + 'howto/multibar', + 'howto/parallel-execution', +]); document.addEventListener('DOMContentLoaded', () => { if (typeof Terminal === 'undefined') return; diff --git a/docs/examples/_registry.py b/docs/examples/_registry.py index ffa07a78..cdfbf211 100644 --- a/docs/examples/_registry.py +++ b/docs/examples/_registry.py @@ -25,6 +25,10 @@ class Demo: term_width: int = 112 #: How many preceding log lines to keep visible above the bar. log_lines: int = 0 + #: Completed output lines retained for line-by-line log recordings. + history_lines: int = 0 + #: Preserve real thread scheduling when concurrency is the demonstration. + capture_real_time: bool = False #: Upper bound on animation frames; excess frames are sampled evenly. max_frames: int = 120 #: Seconds each animation frame stays visible in the rendered SVG. @@ -54,8 +58,8 @@ def svg_path(self) -> pathlib.Path: DEMOS: tuple[Demo, ...] = ( Demo('howto/colors', 'Fixed and gradient bar colors'), - Demo('howto/custom-widget', 'A hand-written widget'), - Demo('howto/dynamic-messages', 'Variable and DynamicMessage'), + Demo('howto/custom-widget', 'The current job phase', term_width=60), + Demo('howto/dynamic-messages', 'Errors found while scanning logs', term_width=60), Demo( 'howto/file-transfer', 'DataSize, FileTransferSpeed, AdaptiveTransferSpeed', @@ -63,9 +67,24 @@ def svg_path(self) -> pathlib.Path: Demo('howto/iterable-wrapper', 'Wrapping an iterable directly'), Demo('howto/logging-integration', 'Logging above the bar', log_lines=2), Demo('howto/multibar', 'MultiBar jobs finishing at different times'), - Demo('howto/multibar-line-offset', 'Manual line-offset bars'), - Demo('howto/non-tty', 'Forcing one line per update'), - Demo('howto/prefix-suffix', 'Templated prefix and suffix'), + Demo('howto/multibar-line-offset', 'Four independent rows', term_width=60), + Demo( + 'howto/non-tty', + 'Keeping each update in a log', + term_width=60, + history_lines=4, + ), + Demo( + 'howto/parallel-execution', + 'Three workers reporting task progress', + term_width=60, + capture_real_time=True, + # Real worker threads must overlap in this demonstration. Their + # redraw order depends on scheduling, so compare the behaviour + # in tests rather than requiring byte-identical recordings. + drift_check=False, + ), + Demo('howto/prefix-suffix', 'Live file and block labels', term_width=60), Demo('howto/redirect-stdout', 'print() above the bar', log_lines=2), Demo('howto/tqdm-style', 'tqdm-style keyword arguments'), Demo('howto/unknown-length', 'UnknownLength with an animated marker'), diff --git a/docs/examples/howto/custom_widget.py b/docs/examples/howto/custom_widget.py index f8223054..9112310f 100644 --- a/docs/examples/howto/custom_widget.py +++ b/docs/examples/howto/custom_widget.py @@ -1,10 +1,7 @@ -"""Write your own widget by subclassing `WidgetBase`. +"""Name each phase of a job with a custom widget. -A widget is a callable: `__call__(self, progress, data)` returns the text -to render for one redraw. `progress` is the bar itself (read from it, don't -mutate it); `data` is the same snapshot dict the built-in widgets read -- -`data['value']`, `data['percentage']`, and so on. This one names the -current phase instead of showing a percentage. +A widget returns the text for one redraw from the bar's data snapshot. +Place the phase before the stretching bar so it stays easy to find. """ import time @@ -13,34 +10,38 @@ from progressbar.bar import ProgressBarMixinBase from progressbar.widgets import Data, WidgetBase -STEPS = 24 +STEPS: int = 100 class Stage(WidgetBase): - """Names the current phase of the job instead of a percentage.""" + """Show the phase that corresponds to the current percentage.""" def __call__(self, progress: ProgressBarMixinBase, data: Data) -> str: - percentage = data['percentage'] or 0.0 + percentage: float = data['percentage'] or 0.0 + phase: str if percentage < 20: - return 'starting' - elif percentage < 90: - return 'working' + phase = 'preparing' + elif percentage < 85: + phase = 'processing' else: - return 'finishing' + phase = 'finishing' + return f'Phase: {phase:10}' def main() -> None: - widgets = [ + widgets: list[str | WidgetBase] = [ + Stage(), + ' ', progressbar.Percentage(), ' ', progressbar.Bar(), - ' ', - Stage(), ] + bar: progressbar.ProgressBar + step: int with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: for step in range(STEPS): + time.sleep(0.02) bar.update(step + 1) - time.sleep(0.005) if __name__ == '__main__': diff --git a/docs/examples/howto/dynamic_messages.py b/docs/examples/howto/dynamic_messages.py index 5405998f..850ac373 100644 --- a/docs/examples/howto/dynamic_messages.py +++ b/docs/examples/howto/dynamic_messages.py @@ -1,36 +1,43 @@ -"""Recognize `DynamicMessage` in old code as today's `Variable`. +"""Count errors while scanning log records with a Variable widget. -Older code may still import `DynamicMessage` -- it is a plain subclass of -`Variable` kept for compatibility, not a different widget. Prefer -`Variable` in new code; this example updates one of each, side by side -from the same value, to show they behave identically. +The bar counts inspected records. The named variable counts only the +records that contain an error, so the two readings describe different +parts of the same job. """ -import random import time import progressbar +from progressbar.widgets import WidgetBase -random.seed(0) - -STEPS = 24 +RECORDS: list[str] = [ + 'INFO request received', + 'INFO cache hit', + 'ERROR request timed out', + 'INFO response sent', + 'INFO connection closed', +] * 20 def main() -> None: - widgets = [ + widgets: list[str | WidgetBase] = [ + progressbar.Variable('errors', format='Errors: {value:2.0f}'), + ' | Scanned ', progressbar.Percentage(), ' ', progressbar.Bar(), - ' ', - progressbar.Variable('current'), - ' ', - progressbar.DynamicMessage('legacy'), ] - with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: - for step in range(STEPS): - value = random.random() - bar.update(step + 1, current=value, legacy=value) - time.sleep(0.005) + errors: int = 0 + bar: progressbar.ProgressBar + step: int + record: str + with progressbar.ProgressBar( + max_value=len(RECORDS), widgets=widgets, variables={'errors': 0} + ) as bar: + for step, record in enumerate(RECORDS, start=1): + errors += record.startswith('ERROR') + time.sleep(0.02) + bar.update(step, errors=errors) if __name__ == '__main__': diff --git a/docs/examples/howto/multibar_line_offset.py b/docs/examples/howto/multibar_line_offset.py index b479ffa3..d96ef700 100644 --- a/docs/examples/howto/multibar_line_offset.py +++ b/docs/examples/howto/multibar_line_offset.py @@ -14,20 +14,32 @@ random.seed(0) -BARS = 4 -STEPS = 20 +BARS: int = 4 +STEPS: int = 20 def main() -> None: print('\n' * BARS, end='') - bars = [ + bars: list[progressbar.ProgressBar] = [ progressbar.ProgressBar( max_value=STEPS, line_offset=index + 1, + prefix=f'Job {index + 1}: ', + widgets=[ + progressbar.Percentage(), + ' ', + progressbar.Bar(), + ' (', + progressbar.SimpleProgress(), + ')', + ], max_error=False, ) for index in range(BARS) ] + bar: progressbar.ProgressBar + for bar in bars: + bar.start() for _ in range(STEPS * BARS): random.choice(bars).increment() time.sleep(0.01) diff --git a/docs/examples/howto/parallel_execution.py b/docs/examples/howto/parallel_execution.py new file mode 100644 index 00000000..6cf9fad6 --- /dev/null +++ b/docs/examples/howto/parallel_execution.py @@ -0,0 +1,57 @@ +"""Report a hundred small steps from each of three concurrent workers. + +The overall bar counts finished files. Each worker supplies its own +known block count through current_task_bar(), so its row shows a +percentage while the other files are still being processed. +""" + +import sys +import time + +import progressbar + +FILES: list[str] = ['users.csv', 'sales.csv', 'stock.csv'] +BLOCKS: int = 100 + + +def process_file(filename: str) -> str: + task_bar: progressbar.ProgressBar | None = progressbar.current_task_bar() + if task_bar is not None: + task_bar.max_value = BLOCKS + task_bar.widgets = [ + f'{filename:10} ', + progressbar.Percentage(), + ' ', + progressbar.Bar(), + ] + delay: float = 0.02 + FILES.index(filename) * 0.004 + block: int + for block in range(BLOCKS): + time.sleep(delay) + if task_bar is not None: + task_bar.update(block + 1) + return filename + + +def main() -> None: + multibar: progressbar.MultiBar = progressbar.MultiBar( + fd=sys.stdout, sort_reverse=False, prepend_label=False + ) + results: list[str] = progressbar.map( + process_file, + FILES, + workers=3, + bar=multibar, + poll_interval=0.02, + widgets=[ + 'Total ', + progressbar.SimpleProgress(), + ' files ', + progressbar.Bar(), + ], + ) + assert results == FILES + + +if __name__ == '__main__': + main() diff --git a/docs/examples/howto/prefix_suffix.py b/docs/examples/howto/prefix_suffix.py index a10d5642..154c1e17 100644 --- a/docs/examples/howto/prefix_suffix.py +++ b/docs/examples/howto/prefix_suffix.py @@ -1,29 +1,37 @@ -"""Template `prefix=`/`suffix=` with the bar's own values, not a fixed string. +"""Show the current file and processed blocks in prefix/suffix templates. -Both accept a `str.format()` template evaluated against the bar's data on -every redraw -- `{value}`, `{max_value}`, or a custom entry seeded through -`variables=` and updated by name through `bar.update()`. Compare a plain -string, which is what most other examples in this set use for their -prefix. +Each file takes twenty small steps. Both templates read the same live +bar data on every redraw, including the filename supplied to update(). """ import time import progressbar -FILES = ['a.txt', 'b.csv', 'c.json', 'd.log', 'e.txt', 'f.csv'] +FILES: list[str] = ['users.csv', 'sales.csv', 'stock.csv', 'costs.csv', 'audit.csv'] +BLOCKS_PER_FILE: int = 20 def main() -> None: + completed: int = 0 + bar: progressbar.ProgressBar + file_number: int + filename: str + block: int with progressbar.ProgressBar( - max_value=len(FILES), - prefix='{variables.filename} ', - suffix=' ({value} of {max_value})', - variables={'filename': '--'}, + max_value=len(FILES) * BLOCKS_PER_FILE, + prefix='{variables.filename} {variables.file_number}/5 ', + suffix=' {value:3}/{max_value} blocks', + variables={'filename': FILES[0], 'file_number': 1}, + widgets=[progressbar.Percentage(), ' ', progressbar.Bar()], ) as bar: - for step, filename in enumerate(FILES): - bar.update(step + 1, filename=filename) - time.sleep(0.1) + for file_number, filename in enumerate(FILES, start=1): + for block in range(BLOCKS_PER_FILE): + time.sleep(0.02) + completed += 1 + bar.update( + completed, filename=filename, file_number=file_number + ) if __name__ == '__main__': diff --git a/docs/howto/custom-widget.rst b/docs/howto/custom-widget.rst index 4486d854..f09ce6e7 100644 --- a/docs/howto/custom-widget.rst +++ b/docs/howto/custom-widget.rst @@ -3,11 +3,15 @@ Write a custom widget ===================== The built-in widgets cover percentages, timers, and transfer speeds, but -not every readout fits that mold -- naming the current phase of a job -("starting", "working", "finishing") isn't something any of them do. +a job may also need a named phase. The custom widget below shows +"preparing", "processing", and "finishing" as the work advances. .. demo:: howto/custom-widget +The phase appears before the bar. ``Stage`` returns a label padded to +ten characters, so switching from "preparing" to "processing" keeps +the percentage and bar aligned. + A widget is any callable matching ``WidgetBase.__call__(self, progress, data)``, returning the text to render for one redraw. Subclass ``WidgetBase`` and implement ``__call__``: ``progress`` is the bar itself diff --git a/docs/howto/dynamic-messages.rst b/docs/howto/dynamic-messages.rst index a3d76e39..4b11b58b 100644 --- a/docs/howto/dynamic-messages.rst +++ b/docs/howto/dynamic-messages.rst @@ -2,19 +2,22 @@ Show a custom value next to the bar =================================== -Sometimes the bar's own progress isn't the only number worth showing -- -a current filename, a running total, or any other value your loop -computes -- and that value doesn't come from ``value``/``max_value`` at -all. +When you scan a log, the number of errors matters alongside the number +of records inspected. A ``Variable`` widget displays the error count +while the percentage tracks the scan: .. demo:: howto/dynamic-messages -``Variable(name)`` renders a named entry from ``bar.update()``'s keyword -arguments: pass ``current=some_value`` to ``update()`` and a -``Variable('current')`` widget picks it up on the next redraw. You don't -need to seed it in advance -- the bar scans its widget list at -construction and registers a placeholder for every named variable that -isn't already supplied, so the first render shows dashes rather than -raising. Older code may import ``DynamicMessage`` instead: it is a plain -subclass of ``Variable``, kept only so existing imports keep working, and -behaves identically -- prefer ``Variable`` in anything new. +``Variable('errors')`` reads the ``errors=`` keyword passed to +``bar.update()``. The example increments that count only when a record +starts with ``ERROR``. The scan finishes at 100% with 20 errors found +among 100 records. Placing the count first keeps it beside the scan +percentage as the bar stretches to fill the remaining width. + +``variables={'errors': 0}`` supplies the initial reading. Without an +initial value, the bar registers a placeholder for each ``Variable`` +in its widget list and displays dashes until the first update. + +Older code may import ``DynamicMessage``. It is a plain subclass of +``Variable``, kept for compatibility, and behaves identically. Use +``Variable`` in new code. diff --git a/docs/howto/index.rst b/docs/howto/index.rst index a301efe8..dd646db5 100644 --- a/docs/howto/index.rst +++ b/docs/howto/index.rst @@ -113,15 +113,15 @@ Manage several jobs .. rubric:: :doc:`Run a batch in parallel ` - Apply a function to several items with ``progressbar.map`` and track the - completed work. Choose threads, processes or asyncio, with one overall bar - or a bar per task. Run these examples locally. + Watch three files advance at different speeds with ``progressbar.map``. + Each worker reports its own progress, while the overall bar counts finished + files. Run this example locally. .. only:: html and not epub .. container:: guide-preview - .. image:: /_static/demos/readme-parallel.svg + .. image:: /_static/demos/howto-parallel-execution.svg :target: parallel-execution.html :alt: progressbar.map running a batch with a separate bar for each active task. @@ -195,8 +195,9 @@ Customise the display .. rubric:: :doc:`Display your own widget ` - Show a job's current phase alongside the built-in widgets. Write a callable - that returns the text for each redraw and add it to the widget list. + Show ``Phase: preparing``, ``processing`` and ``finishing`` as the job + advances. Write a callable that supplies that text and place it before + the bar. .. only:: html and not epub @@ -210,8 +211,9 @@ Customise the display .. rubric:: :doc:`Show a value from your loop ` - Display an extra value that your loop computes. A ``Variable`` widget picks - up its named value from each call to ``bar.update()``. + Count errors while scanning log records. ``Errors`` increases only when a + record contains an error, while the bar tracks every record scanned. + A ``Variable`` widget reads that count from ``bar.update()``. .. only:: html and not epub @@ -219,15 +221,15 @@ Customise the display .. image:: /_static/demos/howto-dynamic-messages.svg :target: dynamic-messages.html - :alt: A named variable changing alongside the progress bar. + :alt: An error count increasing separately from the percentage of records scanned. .. container:: guide-card .. rubric:: :doc:`Put live values in labels ` - Show changing values before or after the bar. Put fields such as ``{value}`` - or a custom variable in a prefix or suffix template to refresh them on each - redraw. + Show the current filename and file number before the bar, with the processed + block count after it. Each file advances in twenty small steps, so you can + watch the labels change as the work moves from one file to the next. .. only:: html and not epub diff --git a/docs/howto/parallel-execution.rst b/docs/howto/parallel-execution.rst index 558f17b2..d3b6cf6a 100644 --- a/docs/howto/parallel-execution.rst +++ b/docs/howto/parallel-execution.rst @@ -18,6 +18,14 @@ exps)``), and the bar counts completed items. The bar keeps animating -- ETA, timers, spinners -- even while long tasks are running with nothing finishing. +.. demo:: howto/parallel-execution + +Three threads process the files concurrently. Each worker gets its own +bar from ``current_task_bar()``, sets ``max_value`` to its hundred +blocks, and updates that bar after each block. The percentages on the +file rows advance while the overall count waits for completed files. +The small sleeps stand in for time spent processing each block. + Choosing where the work runs ============================ diff --git a/docs/howto/prefix-suffix.rst b/docs/howto/prefix-suffix.rst index c22a2680..090ad5d9 100644 --- a/docs/howto/prefix-suffix.rst +++ b/docs/howto/prefix-suffix.rst @@ -8,6 +8,12 @@ running count, needs to be re-evaluated on every redraw. .. demo:: howto/prefix-suffix +The prefix names the current CSV file and its position in the batch. +The suffix counts processed blocks across all five files. Each file +has twenty blocks, so the bar advances a hundred times while the +filename changes only five times. The filename column and block count +keep a fixed width as their values change. + Both ``prefix=`` and ``suffix=`` accept a ``str.format()`` template evaluated against the bar's data on every redraw, not just a plain string: use ``{value}``, ``{max_value}``, or any other key the built-in diff --git a/scripts/render_demos.py b/scripts/render_demos.py index 6c665c37..40544450 100644 --- a/scripts/render_demos.py +++ b/scripts/render_demos.py @@ -91,11 +91,14 @@ def load_docs_examples(repo_root: Path) -> types.ModuleType: # cursor up n lines to column 0) to that bar's row, the freshly rendered # text, then NEXT_LINE(offset) (``ESC[E``) back down to the shared # baseline below every bar (progressbar/multi.py's ``render``/``print``). -# See ``_parse_multibar_frames`` for how ``offset`` is used. +# Manual line offsets repeat F/B once per row. Accept both forms and sum +# the upward movements to recover the row above the shared baseline. MULTIBAR_REPOSITION_RE = re.compile( - r'\x1b\[(\d*)F\r?(.*?)\x1b\[\d*E', + r'(?P(?:\x1b\[\d*[FA])+)\r?' + r'(?P.*?)(?P(?:\x1b\[\d*[EB])+)', re.DOTALL, ) +CURSOR_UP_RE: re.Pattern[str] = re.compile(r'\x1b\[(\d*)[FA]') # Default per-frame duration. Registry entries can override it (and add a # final-frame hold) via ``Demo.frame_seconds``/``Demo.end_hold_seconds``; # the README demos do, since they pace as a first impression rather than @@ -116,7 +119,7 @@ def load_docs_examples(repo_root: Path) -> types.ModuleType: def _demo_argv(demo: Demo) -> list[str]: """Build the argv used to run ``demo`` under capture. - Every demo runs through a small ``-c`` bootstrap that freezes the clock + By default, a demo runs through a ``-c`` bootstrap that freezes the clock (via freezegun, already a test dependency) and makes ``time.sleep`` advance it by exactly the requested amount instead of actually blocking, before executing the module. This lives entirely in the @@ -169,10 +172,21 @@ def _demo_argv(demo: Demo) -> list[str]: concurrent ticks would corrupt freezegun's state; such demos remain scheduling-dependent and keep ``drift_check=False``. + Demos with ``capture_real_time`` keep normal clocks and thread + scheduling so workers can visibly advance concurrently. + Capture disables the minimum redraw interval so real intermediate updates reach the recording. Playback timing comes from the registry. The example source and normal library redraw limits stay unchanged. """ + if demo.capture_real_time: + bootstrap: str = ( + 'import runpy, progressbar\n' + 'progressbar.ProgressBar._MINIMUM_UPDATE_INTERVAL = 0.0\n' + f"runpy.run_path({str(demo.path)!r}, run_name='__main__')\n" + ) + return [sys.executable, '-c', bootstrap] + bootstrap = ( 'import threading, time, runpy, freezegun\n' 'import progressbar.multi\n' @@ -283,7 +297,7 @@ def capture_demo(demo: Demo) -> list[list[str]]: finally: os.close(controller) - frames = parse_frames(output) + frames = parse_frames(output, history_lines=demo.history_lines) if demo.log_lines: frames = keep_recent_logs_with_progress(frames, demo.log_lines) frames = dedupe_consecutive_frames(frames) @@ -312,12 +326,19 @@ def normalize_terminal_line(line: str) -> str: return STRAY_CSI_RE.sub('', line) -def parse_frames(output: str) -> list[list[str]]: +def parse_frames( + output: str, + *, + history_lines: int = 0, +) -> list[list[str]]: output = output.replace('\x1b[2K', '') if MULTIBAR_REPOSITION_RE.search(output): return _parse_multibar_frames(output) + if history_lines: + return _parse_history_frames(output, history_lines) + frames: list[list[str]] = [] if '\f' in output: for raw_frame in output.split('\f'): @@ -344,6 +365,27 @@ def parse_frames(output: str) -> list[list[str]]: return frames +def _parse_history_frames( + output: str, + history_lines: int, +) -> list[list[str]]: + """Retain newline output while carriage returns replace the active row.""" + history: list[str] = [] + frames: list[list[str]] = [] + raw_line: str + for raw_line in output.replace('\r\n', '\n').split('\n'): + last_line: str = '' + part: str + for part in raw_line.split('\r'): + line: str = normalize_terminal_line(part.strip()) + if line: + frames.append((history + [line])[-history_lines:]) + last_line = line + if last_line: + history = (history + [last_line])[-history_lines:] + return frames + + def _parse_multibar_frames(output: str) -> list[list[str]]: """Reconstruct ``MultiBar``'s per-bar redraws into combined frames. @@ -362,9 +404,16 @@ def _parse_multibar_frames(output: str) -> list[list[str]]: lines_by_offset: dict[int, str] = {} frames: list[list[str]] = [] for match in MULTIBAR_REPOSITION_RE.finditer(output): - offset = int(match.group(1) or 1) - text = normalize_terminal_line(match.group(2).strip()) + offset: int = sum( + int(count or 1) + for count in CURSOR_UP_RE.findall(match.group('up')) + ) + text: str = normalize_terminal_line(match.group('text').strip()) if not text: + # Manual offsets also wrap the empty write from finish(). + # That write moves the cursor without erasing the bar. + if match.group('down').endswith('B'): + continue # An empty body at an offset is MultiBar clearing that row -- # its `render` erases the line of a bar that vanished since # the previous frame (e.g. the parallel display deletes each diff --git a/tests/console/test_console.py b/tests/console/test_console.py index 4ca95ece..84410869 100644 --- a/tests/console/test_console.py +++ b/tests/console/test_console.py @@ -28,8 +28,8 @@ xterm entirely). * ``test_multibar_demos_have_no_run_button`` -- ``MultiBar``'s ``with`` form starts a real OS thread, which Pyodide cannot provide - (``Thread.start()`` raises there). ``howto/multibar`` is the one page - in the built site that uses it (``readme/multibar`` is registered for + (``Thread.start()`` raises there). Both ``howto/multibar`` and + ``howto/parallel-execution`` need threads (``readme/multibar`` is registered for SVG rendering only -- ``README.md`` embeds it as a static image for PyPI/GitHub, never through the ``.. demo::`` directive, so it never produces a ``.demo-run`` element to test). The Run button must never @@ -268,13 +268,15 @@ def test_run_button_streams_progress_to_completion( assert not errors, f'console errors during a normal run: {errors}' +@pytest.mark.parametrize('demo_name', ['howto/multibar', 'howto/parallel-execution']) def test_multibar_demos_have_no_run_button( server: str, page: tuple[Page, list[str]], + demo_name: str, ) -> None: browser_page, _errors = page - browser_page.goto(f'{server}/howto/multibar.html') - container = browser_page.locator('.demo-run[data-demo="howto/multibar"]') + browser_page.goto(f'{server}/{demo_name}.html') + container = browser_page.locator(f'.demo-run[data-demo="{demo_name}"]') expect_ = playwright_api.expect expect_(container).to_have_class('demo-run demo-run-unavailable') assert container.locator('.demo-button').count() == 0 diff --git a/tests/test_readme_demos.py b/tests/test_readme_demos.py index 2be258a8..2c59b788 100644 --- a/tests/test_readme_demos.py +++ b/tests/test_readme_demos.py @@ -259,18 +259,18 @@ def test_render_svg_reduced_motion_rule_freezes_on_last_frame( def test_demo_description_strips_single_backtick_markup_from_real_docstring() -> ( # noqa: E501 None ): - # docs/examples/howto/custom_widget.py's docstring uses RST's + # docs/examples/howto/redirect_stdout.py's docstring uses RST's # single-backtick inline-code style (`WidgetBase`) -- unlike the # widgets/*.py docstrings, which use double backticks (``Widget``). # Regression guard: an earlier version of demo_description only # stripped the double-backtick pattern, leaking single backticks # verbatim into 11 of the registry's 50 demos' text. - demo = demos.DEMOS_BY_NAME['howto/custom-widget'] + demo = demos.DEMOS_BY_NAME['howto/redirect-stdout'] description = demos.demo_description(demo) assert '`' not in description - assert 'WidgetBase' in description + assert 'print()' in description def test_demo_description_strips_double_and_single_backtick_markup( @@ -398,6 +398,142 @@ def test_parse_frames_groups_multibar_redraws_by_offset() -> None: ] +def test_parse_frames_groups_repeated_manual_line_offsets() -> None: + output: str = ( + '\x1b[F\x1b[F\rbuild 10%\x1b[B\x1b[B' + '\x1b[F\rtest 5%\x1b[B' + '\x1b[F\x1b[F\rbuild 20%\x1b[B\x1b[B' + '\x1b[F\x1b[F\r\x1b[B\x1b[B' + ) + assert demos.parse_frames(output) == [ + ['build 10%'], + ['build 10%', 'test 5%'], + ['build 20%', 'test 5%'], + ] + + +def test_multibar_clear_removes_only_the_retired_row() -> None: + output: str = ( + '\x1b[2Fbuild 100%\x1b[2E' + '\x1b[1Ftest 50%\x1b[1E' + '\x1b[2F\x1b[2K\x1b[2E' + ) + assert demos.parse_frames(output)[-1] == ['test 50%'] + + +def test_manual_line_offset_capture_shows_four_actual_rows() -> None: + frames: list[list[str]] = demos.capture_demo( + demos.DEMOS_BY_NAME['howto/multibar-line-offset'] + ) + assert any( + len(frame) == 4 and all(_has_bar(line) for line in frame) + for frame in frames + ) + assert len(frames[-1]) == 4 + assert all( + '(20 of 20)' in demos.ANSI_SGR_RE.sub('', line) + for line in frames[-1] + ) + assert min(_bar_widths(frames)) >= 20 + + +def test_parallel_execution_capture_shows_three_active_workers() -> None: + frames: list[list[str]] = demos.capture_demo( + demos.DEMOS_BY_NAME['howto/parallel-execution'] + ) + active_workers: set[str] + frame: list[str] + for frame in frames: + active_workers = set() + line: str + for line in frame: + plain_line: str = demos.ANSI_SGR_RE.sub('', line) + match: re.Match[str] | None = demos.PERCENT_RE.search(plain_line) + if match and 0 < int(match.group()[:-1]) < 100: + active_workers.add(plain_line[:match.start()].strip()) + if len(active_workers) >= 3: + break + else: + pytest.fail('No frame shows three workers making progress together') + last_frame: str = demos.ANSI_SGR_RE.sub('', '\n'.join(frames[-1])) + assert 'Total' in last_frame + assert '3 of 3 files' in last_frame + + +def test_parallel_worker_keeps_filename_after_early_render( + monkeypatch: pytest.MonkeyPatch, +) -> None: + import io + from collections.abc import Callable + + import progressbar + + module: types.ModuleType = demos.load_example( + demos.DEMOS_BY_NAME['howto/parallel-execution'] + ) + + def render_before_worker( + function: Callable[[str], str], + filenames: list[str], + *, + bar: progressbar.MultiBar, + **kwargs: object, + ) -> list[str]: + bar.fd = io.StringIO() + task_bar: progressbar.ProgressBar = progressbar.ProgressBar( + max_value=progressbar.UnknownLength + ) + bar[filenames[0]] = task_bar + task_bar.start() + bar.render(force=True) + monkeypatch.setattr(progressbar, 'current_task_bar', lambda: task_bar) + function(filenames[0]) + assert task_bar._format_line().count(filenames[0]) == 1 + return filenames + + monkeypatch.setattr(progressbar, 'map', render_before_worker) + monkeypatch.setattr(module.time, 'sleep', lambda _: None) + module.main() + + +def test_non_tty_capture_keeps_increasing_updates_in_history() -> None: + frames: list[list[str]] = demos.capture_demo( + demos.DEMOS_BY_NAME['howto/non-tty'] + ) + percentages: list[list[int]] = [ + [ + int(match.group()[:-1]) + for line in frame + if (match := demos.PERCENT_RE.search( + demos.ANSI_SGR_RE.sub('', line) + )) + ] + for frame in frames + ] + assert max(map(len, frames)) == 4 + assert any( + len(values) == 4 + and all(a < b for a, b in zip(values, values[1:])) + for values in percentages + ) + assert percentages[-1][-1] == 100 + + +def test_history_keeps_newlines_but_replaces_carriage_return_updates() -> None: + frames: list[list[str]] = demos.parse_frames( + 'first\r\n0%\r10%\r\n20%\r\n30%\r\n40%', + history_lines=3, + ) + assert frames == [ + ['first'], + ['first', '0%'], + ['first', '10%'], + ['first', '10%', '20%'], + ['10%', '20%', '30%'], + ['20%', '30%', '40%'], + ] + + def test_readme_demos_are_registered_in_display_order() -> None: readme_demos = [ demo for demo in demos.DEMOS if demo.name.startswith('readme/') @@ -653,6 +789,8 @@ def test_capture_demo_reports_a_crashing_example_clearly( term_width=80, log_lines=0, max_frames=24, + capture_real_time=False, + history_lines=0, ) with pytest.raises(SystemExit) as error: @@ -683,6 +821,8 @@ def test_capture_demo_reports_a_hanging_example_clearly( term_width=80, log_lines=0, max_frames=24, + capture_real_time=False, + history_lines=0, ) with pytest.raises(SystemExit) as error: From d1001a60d50843050e163ed661bbe482058e1ed8 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 13 Sep 2026 18:04:36 +0200 Subject: [PATCH 08/18] Expand reference, explanation and installation pages with visual guides --- docs/_static/section-pages.css | 67 +++++++++++++++++++++++++ docs/_templates/page.html | 5 +- docs/explanation/index.rst | 63 ++++++++++++++++++++++- docs/installation.rst | 91 +++++++++++++++++++++++++++++----- docs/reference/index.rst | 88 +++++++++++++++++++++++++++++++- 5 files changed, 299 insertions(+), 15 deletions(-) create mode 100644 docs/_static/section-pages.css diff --git a/docs/_static/section-pages.css b/docs/_static/section-pages.css new file mode 100644 index 00000000..17eb7d81 --- /dev/null +++ b/docs/_static/section-pages.css @@ -0,0 +1,67 @@ +.render-flow > ol, +.install-options { + display: grid; + gap: 16px; + margin: 24px 0; + padding: 0; +} +.render-flow > ol { + grid-template-columns: repeat(3, minmax(0, 1fr)); + list-style: none; + counter-reset: render-step; +} +.render-flow > ol > li, +[role="main"] .install-option.container { + box-sizing: border-box; + min-width: 0; + padding: 20px; + border: 1px solid var(--color-background-border); + border-radius: 10px; + background: var(--home-card-transfer); +} +.render-flow > ol > li { + counter-increment: render-step; +} +.render-flow > ol > li::before { + content: counter(render-step); + display: block; + margin-bottom: 12px; + color: var(--home-accent); + font-size: 1.6rem; + font-weight: 750; +} +.render-flow > ol > li:nth-child(2), +[role="main"] .install-option.container:nth-child(2) { + background: var(--home-card-jobs); +} +.render-flow > ol > li:nth-child(3) { + background: var(--home-card-logs); +} +.render-flow p, +.install-option p { + margin: 0 0 12px; +} +.render-flow p:last-child, +.install-option > :last-child { + margin-bottom: 0; +} +.install-options { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.install-option .rubric { + margin-top: 0; + font-size: 1.1rem; +} +.install-option div[class*="highlight"] { + margin: 16px 0 0; +} +.install-option pre { + padding: 12px; + font-size: 0.8rem; +} +@media (max-width: 640px) { + .render-flow > ol, + .install-options { + grid-template-columns: 1fr; + } +} diff --git a/docs/_templates/page.html b/docs/_templates/page.html index 9c50af10..fc4d4418 100644 --- a/docs/_templates/page.html +++ b/docs/_templates/page.html @@ -5,9 +5,12 @@ {% if pagename == 'index' and builder == 'html' %} {% endif %} -{% if pagename in ('tutorial/index', 'howto/index') and builder == 'html' %} +{% if pagename in ('tutorial/index', 'howto/index', 'reference/index', 'explanation/index', 'installation') and builder == 'html' %} {% endif %} +{% if pagename in ('reference/index', 'explanation/index', 'installation') and builder == 'html' %} + +{% endif %} {% endblock %} {% block body %} diff --git a/docs/explanation/index.rst b/docs/explanation/index.rst index 0fe1241f..3eb003f5 100644 --- a/docs/explanation/index.rst +++ b/docs/explanation/index.rst @@ -2,9 +2,70 @@ Explanation =========== -Background on how the library works and why it is built this way. +If a bar redraws less often than your loop runs, changes its output when +piped to a file, or selects a different renderer, these guides explain why. + +From an update to a line +======================== + +A progress update usually follows these three steps. Forced updates can +bypass the redraw checks, including the updates made when a bar starts +and finishes. + +.. container:: render-flow + + 1. **Record progress** + + Keep the current value when your loop advances or you call + ``bar.update(value)``. + + 2. **Check whether a redraw is due** + + An integer threshold skips most of the work in a fast loop. + Calls that pass it are checked against timing and visible progress. + + 3. **Format and write the line** + + When a redraw is due, build the display from widgets or the fast + renderer's fixed format and write it to the output stream. + +Understand the behaviour +======================== + +.. container:: guide-card + + .. rubric:: :doc:`Why doesn't every update redraw? ` + + Follow the integer threshold and timing checks that decide when a value + change produces output. See how ``min_poll_interval``, ``poll_interval`` + and ``force=True`` affect the result. + +.. container:: guide-card + + .. rubric:: :doc:`Why does output change between terminals and logs? ` + + Trace terminal detection, colour support and width selection separately. + Find which constructor arguments and environment variables control + overwriting a line, using colour and sizing the bar. + +.. container:: guide-card + + .. rubric:: :doc:`What work does the fast path skip? ` + + Separate the update gate, the automatically selected ``FastProgressBar`` + renderer and the optional native iterator. Learn when custom widgets + need the full renderer and what the ``fast`` extra changes. + +.. container:: guide-card + + .. rubric:: :doc:`What carries over from the original progressbar? ` + + Check the shared import name and bar lifecycle, then find the modern + names for deprecated arguments and attributes. See where compatibility + ends, including Python version support and newer APIs. .. toctree:: + :hidden: :maxdepth: 1 rendering-and-the-update-gate diff --git a/docs/installation.rst b/docs/installation.rst index 173550bc..e81d6b7a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -2,31 +2,98 @@ Installation ============ -The package is named ``progressbar2`` on PyPI (the module you import is -``progressbar``) and requires Python 3.10 or later. Install it with pip: +Install the ``progressbar2`` package, then import it as ``progressbar``. +You need Python 3.10 or later. + +1. Install the package +====================== + +Choose the command that matches how you manage your Python environment: + +.. container:: install-options + + .. container:: install-option + + .. rubric:: Install with pip + + Use the Python environment where you will run your script: + + .. code-block:: console + + $ python -m pip install progressbar2 + + .. container:: install-option + + .. rubric:: Add to a uv project + + From an existing uv project, add the package as a dependency: + + .. code-block:: console + + $ uv add progressbar2 + +2. Check the import +=================== + +Print the installed version from the same Python environment: .. code-block:: console - $ pip install progressbar2 + $ python -c "import progressbar; print(progressbar.__version__)" -Or with uv: +In a uv project, run the check through uv: .. code-block:: console - $ uv add progressbar2 + $ uv run python -c "import progressbar; print(progressbar.__version__)" + +A version number confirms that Python can import the installed package. + +3. Run your first bar +===================== + +Wrap a loop to track its progress: -The optional ``fast`` extra installs the native iterator accelerator used -by the fast path (see :doc:`explanation/performance-and-the-fast-path`): +.. demo:: tutorial/step1 + +The loop processes 100 steps. ``progressbar.progressbar()`` starts the bar, +advances it as the loop runs and finishes it when the loop ends. The short +sleep makes the movement visible. Replace it with your own work. + +Optional native iterator +======================== + +The standard installation includes the update gate and automatic fast +renderer. The optional ``fast`` extra adds a native iterator for counting +items in large loops: .. code-block:: console - $ pip install 'progressbar2[fast]' + $ python -m pip install 'progressbar2[fast]' -Confirm the install by printing the version: +For a uv project: .. code-block:: console - $ python -c "import progressbar; print(progressbar.__version__)" + $ uv add 'progressbar2[fast]' + +The examples work without this extra. The +:doc:`fast-path explanation ` +describes when the native iterator is used. + +Keep building +============= + +.. container:: guide-card + + .. rubric:: :doc:`Build a bar in five steps ` + + Start with the loop above, then control updates, set a total, choose + widgets and print messages above the display. + +.. container:: guide-card + + .. rubric:: :doc:`Find a guide for your task ` -If that prints a version number such as ``4.6.0``, continue with -:doc:`tutorial/index`. + Track file transfers, show several jobs together or add live values and + colour to your progress display. diff --git a/docs/reference/index.rst b/docs/reference/index.rst index ee9f1066..f19f9bc3 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -2,9 +2,95 @@ Reference ========= -Complete descriptions of the public API surface. +Find constructor arguments, methods and command-line options for the part +of progressbar2 you are using. Each reference explains the available controls +and links to the corresponding classes and functions. + +Bars and parallel work +====================== + +.. container:: guide-card + + .. rubric:: :doc:`Configure a ProgressBar ` + + Set the value range, widgets, output stream and redraw intervals. + Look up ``ProgressBar`` methods and the related ``DataTransferBar``, + ``NullBar`` and ``FastProgressBar`` classes. + + .. only:: html and not epub + + .. container:: guide-preview + + .. image:: /_static/demos/tutorial-step4.svg + :target: progressbar.html + :alt: A ProgressBar configured with a percentage, a bar and an ETA. + +.. container:: guide-card + + .. rubric:: :doc:`Manage rows with MultiBar ` + + Control labels, row ordering and the display of waiting and finished + jobs. ``MultiBar`` keeps the child bars together and redraws them from + its background thread. + + .. only:: html and not epub + + .. container:: guide-preview + + .. image:: /_static/demos/howto-multibar.svg + :target: multibar.html + :alt: MultiBar displaying several jobs as they advance and finish. + +.. container:: guide-card + + .. rubric:: :doc:`Choose a parallel execution API ` + + Look up ``map``, ``imap``, ``amap`` and the other batch helpers. + Compare worker pools, result ordering, concurrency limits and error + handling, including reusable ``Pool`` and ``AsyncPool`` objects. + + .. only:: html and not epub + + .. container:: guide-preview + + .. image:: /_static/demos/howto-parallel-execution.svg + :target: parallel.html + :alt: Parallel tasks with individual progress bars and an overall count. + +Widgets, commands and modules +============================= + +.. container:: guide-card + + .. rubric:: :doc:`Find the widget for your display <../widgets/index>` + + Choose a percentage, timer, transfer speed or live value by what you need + to show. The widget table identifies which displays need a known total, + with a runnable example for each widget. + + ``Percentage``, ``Bar``, ``ETA``, ``Variable`` + +.. container:: guide-card + + .. rubric:: :doc:`Look up command-line options ` + + Track bytes or lines moving through a pipe or between files with the + ``progressbar`` command. Check size, rate and output options, including + the compatibility flags that are accepted but have no effect. + + ``progressbar`` and ``bar`` name the same command. + +.. container:: guide-card + + .. rubric:: :doc:`Browse the full module listing <../progressbar>` + + Find classes, functions and modules in the generated API documentation, + including helpers beyond the focused reference pages above. + + ``progressbar.bar``, ``progressbar.multi``, ``progressbar.widgets`` .. toctree:: + :hidden: :maxdepth: 1 progressbar From 771f298736a6638297014c0e824518e769cb3f5d Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 13 Sep 2026 19:55:01 +0200 Subject: [PATCH 09/18] Fix documentation CI dependencies and lint checks --- docs/examples/_registry.py | 6 +++++- docs/examples/howto/prefix_suffix.py | 12 +++++++++--- progressbar/utils.py | 16 ++++++++-------- pyproject.toml | 1 + scripts/render_demos.py | 4 ++-- tests/console/test_console.py | 10 ++++++---- tests/console/test_homepage.py | 12 +++++++++--- tests/console/test_showcase_assets.py | 6 ++++-- tests/test_readme_demos.py | 23 +++++++++++------------ 9 files changed, 55 insertions(+), 35 deletions(-) diff --git a/docs/examples/_registry.py b/docs/examples/_registry.py index cdfbf211..3bc9f343 100644 --- a/docs/examples/_registry.py +++ b/docs/examples/_registry.py @@ -59,7 +59,11 @@ def svg_path(self) -> pathlib.Path: DEMOS: tuple[Demo, ...] = ( Demo('howto/colors', 'Fixed and gradient bar colors'), Demo('howto/custom-widget', 'The current job phase', term_width=60), - Demo('howto/dynamic-messages', 'Errors found while scanning logs', term_width=60), + Demo( + 'howto/dynamic-messages', + 'Errors found while scanning logs', + term_width=60, + ), Demo( 'howto/file-transfer', 'DataSize, FileTransferSpeed, AdaptiveTransferSpeed', diff --git a/docs/examples/howto/prefix_suffix.py b/docs/examples/howto/prefix_suffix.py index 154c1e17..892d0df5 100644 --- a/docs/examples/howto/prefix_suffix.py +++ b/docs/examples/howto/prefix_suffix.py @@ -8,7 +8,13 @@ import progressbar -FILES: list[str] = ['users.csv', 'sales.csv', 'stock.csv', 'costs.csv', 'audit.csv'] +FILES: list[str] = [ + 'users.csv', + 'sales.csv', + 'stock.csv', + 'costs.csv', + 'audit.csv', +] BLOCKS_PER_FILE: int = 20 @@ -17,7 +23,7 @@ def main() -> None: bar: progressbar.ProgressBar file_number: int filename: str - block: int + _block: int with progressbar.ProgressBar( max_value=len(FILES) * BLOCKS_PER_FILE, prefix='{variables.filename} {variables.file_number}/5 ', @@ -26,7 +32,7 @@ def main() -> None: widgets=[progressbar.Percentage(), ' ', progressbar.Bar()], ) as bar: for file_number, filename in enumerate(FILES, start=1): - for block in range(BLOCKS_PER_FILE): + for _block in range(BLOCKS_PER_FILE): time.sleep(0.02) completed += 1 bar.update( diff --git a/progressbar/utils.py b/progressbar/utils.py index b8e680d6..5be29b20 100644 --- a/progressbar/utils.py +++ b/progressbar/utils.py @@ -128,13 +128,13 @@ def deltas_to_seconds( def no_color(value: StringT) -> StringT: - """Return the `value` without ANSI escape codes. + r"""Return the `value` without ANSI escape codes. - >>> no_color(b'\\x1b[1234]abc') + >>> no_color(b'\x1b[1234]abc') b'abc' - >>> str(no_color('\\x1b[1234]abc')) + >>> str(no_color('\x1b[1234]abc')) 'abc' - >>> str(no_color('\\x1b[1234]abc')) + >>> str(no_color('\x1b[1234]abc')) 'abc' >>> no_color(123) Traceback (most recent call last): @@ -158,13 +158,13 @@ def no_color(value: StringT) -> StringT: def len_color(value: types.StringTypes) -> int: - """Return the length of `value` without ANSI escape codes. + r"""Return the length of `value` without ANSI escape codes. - >>> len_color(b'\\x1b[1234]abc') + >>> len_color(b'\x1b[1234]abc') 3 - >>> len_color('\\x1b[1234]abc') + >>> len_color('\x1b[1234]abc') 3 - >>> len_color('\\x1b[1234]abc') + >>> len_color('\x1b[1234]abc') 3 """ return len(no_color(value)) diff --git a/pyproject.toml b/pyproject.toml index 5797b8ea..0d945cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,7 @@ docs-tests = [ 'playwright>=1.48.0', 'pytest-cov>=2.6.1', 'pytest>=4.6.9', + 'sphinx>=1.8.5', ] [dependency-groups] diff --git a/scripts/render_demos.py b/scripts/render_demos.py index 40544450..fbd88294 100644 --- a/scripts/render_demos.py +++ b/scripts/render_demos.py @@ -379,10 +379,10 @@ def _parse_history_frames( for part in raw_line.split('\r'): line: str = normalize_terminal_line(part.strip()) if line: - frames.append((history + [line])[-history_lines:]) + frames.append([*history, line][-history_lines:]) last_line = line if last_line: - history = (history + [last_line])[-history_lines:] + history = [*history, last_line][-history_lines:] return frames diff --git a/tests/console/test_console.py b/tests/console/test_console.py index 84410869..d2cf26ca 100644 --- a/tests/console/test_console.py +++ b/tests/console/test_console.py @@ -29,10 +29,10 @@ * ``test_multibar_demos_have_no_run_button`` -- ``MultiBar``'s ``with`` form starts a real OS thread, which Pyodide cannot provide (``Thread.start()`` raises there). Both ``howto/multibar`` and - ``howto/parallel-execution`` need threads (``readme/multibar`` is registered for - SVG rendering only -- ``README.md`` embeds it as a static image for + ``howto/parallel-execution`` need threads. ``readme/multibar`` is only + registered for SVG rendering -- ``README.md`` embeds a static image for PyPI/GitHub, never through the ``.. demo::`` directive, so it never - produces a ``.demo-run`` element to test). The Run button must never + produces a ``.demo-run`` element to test. The Run button must never be offered there, and the contrast case (``howto/multibar-line-offset``, which does not use the threaded form) must still get one -- otherwise this test would pass whether or @@ -268,7 +268,9 @@ def test_run_button_streams_progress_to_completion( assert not errors, f'console errors during a normal run: {errors}' -@pytest.mark.parametrize('demo_name', ['howto/multibar', 'howto/parallel-execution']) +@pytest.mark.parametrize( + 'demo_name', ['howto/multibar', 'howto/parallel-execution'] +) def test_multibar_demos_have_no_run_button( server: str, page: tuple[Page, list[str]], diff --git a/tests/console/test_homepage.py b/tests/console/test_homepage.py index 9c231bd7..a109a13f 100644 --- a/tests/console/test_homepage.py +++ b/tests/console/test_homepage.py @@ -66,12 +66,16 @@ def test_showcase_keyboard_updates_recording_title_and_guide( tabs.first.focus() browser_page.keyboard.press('ArrowRight') playwright_api.expect(tabs.nth(1)).to_be_focused() - playwright_api.expect(tabs.nth(1)).to_have_attribute('aria-selected', 'true') + playwright_api.expect(tabs.nth(1)).to_have_attribute( + 'aria-selected', 'true' + ) playwright_api.expect(recording).to_have_attribute( 'data', '_static/demos/readme-multibar.svg' ) playwright_api.expect(title).to_have_text('Several jobs in one terminal') - playwright_api.expect(guide).to_have_attribute('href', 'howto/multibar.html') + playwright_api.expect(guide).to_have_attribute( + 'href', 'howto/multibar.html' + ) browser_page.keyboard.press('End') playwright_api.expect(tabs.last).to_be_focused() playwright_api.expect(guide).to_have_attribute( @@ -186,7 +190,9 @@ def test_showcase_reduced_motion_and_failed_recording_keep_guides( browser_page.route('**/readme-multibar.svg', _missing) browser_page.get_by_role('tab', name='Multiple jobs').click() guide: Locator = browser_page.locator('#showcase-guide') - playwright_api.expect(guide).to_have_attribute('href', 'howto/multibar.html') + playwright_api.expect(guide).to_have_attribute( + 'href', 'howto/multibar.html' + ) playwright_api.expect(guide).to_be_visible() playwright_api.expect( browser_page.locator('#showcase-pause') diff --git a/tests/console/test_showcase_assets.py b/tests/console/test_showcase_assets.py index 7b57703f..efe0cc00 100644 --- a/tests/console/test_showcase_assets.py +++ b/tests/console/test_showcase_assets.py @@ -1,4 +1,4 @@ -"""The homepage's raw HTML recordings fail the build when an asset is absent.""" +"""Reject missing homepage recording assets during the build.""" from __future__ import annotations @@ -14,7 +14,9 @@ ROOT: pathlib.Path = pathlib.Path(__file__).resolve().parents[2] -@pytest.mark.parametrize('name', ['readme/colors', 'readme/multibar', 'readme/hero']) +@pytest.mark.parametrize( + 'name', ['readme/colors', 'readme/multibar', 'readme/hero'] +) def test_missing_showcase_recording_is_a_build_error( name: str, tmp_path: pathlib.Path, diff --git a/tests/test_readme_demos.py b/tests/test_readme_demos.py index 2c59b788..65b459c6 100644 --- a/tests/test_readme_demos.py +++ b/tests/test_readme_demos.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools import os import re import sys @@ -414,9 +415,7 @@ def test_parse_frames_groups_repeated_manual_line_offsets() -> None: def test_multibar_clear_removes_only_the_retired_row() -> None: output: str = ( - '\x1b[2Fbuild 100%\x1b[2E' - '\x1b[1Ftest 50%\x1b[1E' - '\x1b[2F\x1b[2K\x1b[2E' + '\x1b[2Fbuild 100%\x1b[2E\x1b[1Ftest 50%\x1b[1E\x1b[2F\x1b[2K\x1b[2E' ) assert demos.parse_frames(output)[-1] == ['test 50%'] @@ -431,8 +430,7 @@ def test_manual_line_offset_capture_shows_four_actual_rows() -> None: ) assert len(frames[-1]) == 4 assert all( - '(20 of 20)' in demos.ANSI_SGR_RE.sub('', line) - for line in frames[-1] + '(20 of 20)' in demos.ANSI_SGR_RE.sub('', line) for line in frames[-1] ) assert min(_bar_widths(frames)) >= 20 @@ -450,7 +448,7 @@ def test_parallel_execution_capture_shows_three_active_workers() -> None: plain_line: str = demos.ANSI_SGR_RE.sub('', line) match: re.Match[str] | None = demos.PERCENT_RE.search(plain_line) if match and 0 < int(match.group()[:-1]) < 100: - active_workers.add(plain_line[:match.start()].strip()) + active_workers.add(plain_line[: match.start()].strip()) if len(active_workers) >= 3: break else: @@ -504,16 +502,17 @@ def test_non_tty_capture_keeps_increasing_updates_in_history() -> None: [ int(match.group()[:-1]) for line in frame - if (match := demos.PERCENT_RE.search( - demos.ANSI_SGR_RE.sub('', line) - )) + if ( + match := demos.PERCENT_RE.search( + demos.ANSI_SGR_RE.sub('', line) + ) + ) ] for frame in frames ] assert max(map(len, frames)) == 4 assert any( - len(values) == 4 - and all(a < b for a, b in zip(values, values[1:])) + len(values) == 4 and all(a < b for a, b in itertools.pairwise(values)) for values in percentages ) assert percentages[-1][-1] == 100 @@ -582,7 +581,7 @@ def test_tutorial_recording_preserves_intermediate_progress() -> None: assert percentages[0] == 0 assert percentages[-1] == 100 assert len(set(percentages)) >= 80 - assert max(b - a for a, b in zip(percentages, percentages[1:])) <= 2 + assert max(b - a for a, b in itertools.pairwise(percentages)) <= 2 # Demos whose entire purpose is a time-derived reading (an elapsed duration, From d80a53bb403dc90c6b592f497858b99dc211f3b0 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 13 Sep 2026 20:22:08 +0200 Subject: [PATCH 10/18] Keep syntax colours visible in the homepage example --- docs/_static/home.css | 5 +++- docs/_static/livecode/livecode.js | 36 +++++++++++++++++++++++++++- tests/console/test_homepage.py | 40 +++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/_static/home.css b/docs/_static/home.css index de32fd7a..672e373f 100644 --- a/docs/_static/home.css +++ b/docs/_static/home.css @@ -79,12 +79,15 @@ .home-quickstart .demo-animation { display: none; } .home-quickstart .demo-source { max-height: 330px; overflow: auto; } .home-quickstart .demo-source pre { font-size: 12px; } -.home-quickstart .demo:has(.demo-editor) .demo-source { display: none; } +.home-quickstart .demo:has(.demo-editor:not([hidden])) .demo-source { display: none; } +.home-quickstart .demo-editor[hidden] { display: none; } .home-quickstart .demo-editor { font-size: 12px; line-height: 1.6; max-height: 400px; } .home-quickstart .demo-terminal { display: none; } .home-quickstart .demo-run[data-started] .demo-terminal { display: block; } .home-quickstart .demo-controls { border-top: 0; padding: 11px 14px; } .home-quickstart .demo-button { padding: 6px 16px; font: 13px var(--font-stack); } +.home-quickstart .demo-edit { padding: 6px 12px; font: 13px var(--font-stack); border: 1px solid var(--color-background-border); border-radius: 4px; background: var(--color-background-primary); color: var(--color-foreground-primary); cursor: pointer; } +.home-quickstart .demo-edit:hover { border-color: var(--home-accent); } .home-quickstart .demo-run:not(:has(.demo-editor)) { padding: 12px; font-size: 13px; } .home-guide-links { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 48px 40px 42px; padding-top: 27px; border-top: 1px solid var(--color-background-border); } .home-guide-links a { text-decoration: none; } diff --git a/docs/_static/livecode/livecode.js b/docs/_static/livecode/livecode.js index 15a4985a..41b5fba6 100644 --- a/docs/_static/livecode/livecode.js +++ b/docs/_static/livecode/livecode.js @@ -91,6 +91,38 @@ function finishRun() { if (activePanel) activePanel.setStatus('idle'); } +/** + * @param {HTMLElement} container + * @param {HTMLTextAreaElement} editor + * @param {HTMLElement} controls + * @param {string} source + */ +function keepHighlightedHomepageSource(container, editor, controls, source) { + if (!container.closest('.home-quickstart')) return; + + editor.hidden = true; + editor.id = 'home-example-editor'; + /** @type {HTMLButtonElement} */ + const edit = document.createElement('button'); + edit.className = 'demo-edit'; + edit.type = 'button'; + edit.textContent = 'Edit code'; + edit.setAttribute('aria-controls', editor.id); + edit.setAttribute('aria-expanded', 'false'); + edit.addEventListener('click', () => { + editor.hidden = !editor.hidden; + edit.textContent = editor.hidden ? 'Edit code' : 'Reset example'; + edit.setAttribute('aria-expanded', String(!editor.hidden)); + if (editor.hidden) { + editor.value = source; + edit.focus(); + } else { + editor.focus(); + } + }); + controls.append(edit); +} + function createPanel(container, source) { const editor = document.createElement('textarea'); editor.className = 'demo-editor'; @@ -114,7 +146,9 @@ function createPanel(container, source) { const controls = document.createElement('div'); controls.className = 'demo-controls'; - controls.append(button, status); + controls.append(button); + keepHighlightedHomepageSource(container, editor, controls, source); + controls.append(status); container.append(controls, editor, screen); const terminal = new Terminal({ diff --git a/tests/console/test_homepage.py b/tests/console/test_homepage.py index a109a13f..6fcf0482 100644 --- a/tests/console/test_homepage.py +++ b/tests/console/test_homepage.py @@ -44,6 +44,18 @@ def record_download(request: Request) -> None: browser_page.goto(f'{server}/index.html') button: Locator = browser_page.locator('.home-quickstart .demo-button') playwright_api.expect(button).to_be_visible() + source: Locator = browser_page.locator('.home-quickstart .demo-source') + playwright_api.expect(source).to_be_visible() + theme: str + for theme in ('light', 'dark'): + browser_page.locator('body').evaluate( + '(body, theme) => body.dataset.theme = theme', theme + ) + assert source.locator('.kn').first.evaluate( + '(node) => getComputedStyle(node).color' + ) != source.locator('pre').evaluate( + '(node) => getComputedStyle(node).color' + ) assert _worker_count(browser_page) == 0 assert not downloads button.click() @@ -52,6 +64,34 @@ def record_download(request: Request) -> None: assert not page[1] +def test_homepage_can_edit_run_and_reset_the_example( + server: str, + page: tuple[Page, list[str]], +) -> None: + browser_page: Page = page[0] + browser_page.goto(f'{server}/index.html') + source: Locator = browser_page.locator('.home-quickstart .demo-source') + editor: Locator = browser_page.get_by_role( + 'textbox', name='Python example source' + ) + edit: Locator = browser_page.get_by_role('button', name='Edit code') + edit.click() + playwright_api.expect(editor).to_be_focused() + playwright_api.expect(source).to_be_hidden() + original: str = editor.input_value() + editor.fill("print('Edited example ran')") + browser_page.get_by_role('button', name='Run', exact=True).click() + _wait_for_terminal_text( + browser_page, 'Edited example ran', BOOT_TIMEOUT_MS + ) + browser_page.get_by_role('button', name='Reset example').click() + playwright_api.expect(source).to_be_visible() + playwright_api.expect(editor).to_be_hidden() + playwright_api.expect(edit).to_be_focused() + assert browser_page.locator('.demo-editor').input_value() == original + assert not page[1] + + def test_showcase_keyboard_updates_recording_title_and_guide( server: str, page: tuple[Page, list[str]], From 7c0e36287f0d9113e5d8bbe498bf9553382da97e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Sun, 13 Sep 2026 20:30:25 +0200 Subject: [PATCH 11/18] Version page assets to refresh cached documentation styles --- docs/_ext/page_assets.py | 38 ++++++++++++++++++++++++++++++++++ docs/_templates/page.html | 20 ------------------ docs/conf.py | 1 + tests/console/test_homepage.py | 8 +++++++ 4 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 docs/_ext/page_assets.py diff --git a/docs/_ext/page_assets.py b/docs/_ext/page_assets.py new file mode 100644 index 00000000..971c7380 --- /dev/null +++ b/docs/_ext/page_assets.py @@ -0,0 +1,38 @@ +"""Register page assets with Sphinx so their URLs include content checksums.""" + +from __future__ import annotations + +import typing + +if typing.TYPE_CHECKING: + from docutils import nodes + from sphinx.application import Sphinx + + +def add_page_assets( + app: Sphinx, + pagename: str, + _templatename: str, + _context: dict[str, typing.Any], + _doctree: nodes.document | None, +) -> None: + if app.builder is None or app.builder.name != 'html': + return + if pagename == 'index': + app.add_css_file('home.css', priority=900) + app.add_js_file('showcase.js', loading_method='defer', priority=900) + if pagename in ( + 'tutorial/index', + 'howto/index', + 'reference/index', + 'explanation/index', + 'installation', + ): + app.add_css_file('guide-cards.css', priority=900) + if pagename in ('reference/index', 'explanation/index', 'installation'): + app.add_css_file('section-pages.css', priority=900) + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect('html-page-context', add_page_assets) + return {'parallel_read_safe': True, 'parallel_write_safe': True} diff --git a/docs/_templates/page.html b/docs/_templates/page.html index fc4d4418..abc999e6 100644 --- a/docs/_templates/page.html +++ b/docs/_templates/page.html @@ -1,18 +1,5 @@ {% extends "!page.html" %} -{% block extra_styles %} -{{ super() }} -{% if pagename == 'index' and builder == 'html' %} - -{% endif %} -{% if pagename in ('tutorial/index', 'howto/index', 'reference/index', 'explanation/index', 'installation') and builder == 'html' %} - -{% endif %} -{% if pagename in ('reference/index', 'explanation/index', 'installation') and builder == 'html' %} - -{% endif %} -{% endblock %} - {% block body %} {% if pagename == 'index' and builder == 'html' %} -{% endif %} -{% endblock %} diff --git a/docs/conf.py b/docs/conf.py index 204849be..acab8ad0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -47,6 +47,7 @@ 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'demo', + 'page_assets', ] # `sphinx-autodoc-typehints` is a declared docs dependency but is diff --git a/tests/console/test_homepage.py b/tests/console/test_homepage.py index 6fcf0482..961f78a9 100644 --- a/tests/console/test_homepage.py +++ b/tests/console/test_homepage.py @@ -29,6 +29,13 @@ def _missing(route: Route) -> None: route.fulfill(status=404, body='not found') +def _stale_home_styles(route: Route) -> None: + route.fulfill( + content_type='text/css', + body='.home-quickstart .demo-source { display: none; }', + ) + + def test_homepage_loads_python_only_after_run( server: str, page: tuple[Page, list[str]], @@ -41,6 +48,7 @@ def record_download(request: Request) -> None: downloads.append(request.url) browser_page.on('request', record_download) + browser_page.route('**/_static/home.css', _stale_home_styles) browser_page.goto(f'{server}/index.html') button: Locator = browser_page.locator('.home-quickstart .demo-button') playwright_api.expect(button).to_be_visible() From 8de347e2c4523bfd746ce937bc254b61751cf18e Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 14 Sep 2026 02:41:06 +0200 Subject: [PATCH 12/18] Show coloured progress in a responsive homepage terminal --- docs/_ext/demo.py | 1 + docs/_static/demos/homepage-quickstart.svg | 60 ++++++++++++++++++++++ docs/_static/livecode/livecode.css | 5 ++ docs/_static/livecode/livecode.js | 38 ++++++++++++-- docs/_static/vendor/LICENSE-addon-fit.txt | 19 +++++++ docs/_static/vendor/README.md | 11 ++++ docs/_static/vendor/addon-fit.js | 2 + docs/_static/vendor/addon-fit.js.map | 1 + docs/examples/_registry.py | 3 ++ docs/examples/homepage/__init__.py | 1 + docs/examples/homepage/quickstart.py | 17 ++++++ docs/index.rst | 4 +- tests/console/test_homepage.py | 41 ++++++++++++++- 13 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 docs/_static/demos/homepage-quickstart.svg create mode 100644 docs/_static/vendor/LICENSE-addon-fit.txt create mode 100644 docs/_static/vendor/addon-fit.js create mode 100644 docs/_static/vendor/addon-fit.js.map create mode 100644 docs/examples/homepage/__init__.py create mode 100644 docs/examples/homepage/quickstart.py diff --git a/docs/_ext/demo.py b/docs/_ext/demo.py index 5647f179..95616d08 100644 --- a/docs/_ext/demo.py +++ b/docs/_ext/demo.py @@ -174,5 +174,6 @@ def setup(app: Sphinx) -> dict[str, typing.Any]: app.add_css_file('vendor/xterm.css') app.add_css_file('livecode/livecode.css') app.add_js_file('vendor/xterm.js') + app.add_js_file('vendor/addon-fit.js') app.add_js_file('livecode/livecode.js') return {'parallel_read_safe': True, 'parallel_write_safe': True} diff --git a/docs/_static/demos/homepage-quickstart.svg b/docs/_static/demos/homepage-quickstart.svg new file mode 100644 index 00000000..beac89c7 --- /dev/null +++ b/docs/_static/demos/homepage-quickstart.svg @@ -0,0 +1,60 @@ + + A percentage and a coloured bar + Show a percentage and a bar that changes colour as work progresses. + + + + + + A percentage and a coloured bar + 0% | | 2% |# | 4% |## | 6% |### | 7% |### | 9% |#### | 11% |##### | 12% |###### | 14% |####### | 16% |######## | 17% |######## | 19% |######### | 21% |########## | 23% |########### | 24% |############ | 26% |############# | 28% |############## | 29% |############### | 31% |################ | 33% |################# | 34% |################# | 36% |################## | 38% |################### | 39% |#################### | 41% |##################### | 43% |###################### | 45% |####################### | 46% |####################### | 48% |######################## | 50% |########################## | 51% |########################## | 53% |########################### | 55% |############################ | 56% |############################# | 58% |############################## | 60% |############################### | 62% |################################ | 63% |################################ | 65% |################################# | 67% |################################## | 68% |################################### | 70% |#################################### | 72% |##################################### | 73% |##################################### | 75% |####################################### | 77% |######################################## | 78% |######################################## | 80% |######################################### | 82% |########################################## | 84% |########################################### | 85% |############################################ | 87% |############################################# | 89% |############################################## | 90% |############################################## | 92% |############################################### | 94% |################################################ | 95% |################################################# | 97% |################################################## | 99% |################################################### |100% |####################################################| + diff --git a/docs/_static/livecode/livecode.css b/docs/_static/livecode/livecode.css index 084b8c17..e2a4f227 100644 --- a/docs/_static/livecode/livecode.css +++ b/docs/_static/livecode/livecode.css @@ -30,6 +30,7 @@ .demo-controls { display: flex; + flex-wrap: wrap; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; @@ -73,3 +74,7 @@ padding: 0.5rem; overflow-x: auto; } + +.demo-terminal .xterm-viewport { + overflow-y: auto; +} diff --git a/docs/_static/livecode/livecode.js b/docs/_static/livecode/livecode.js index 41b5fba6..0c084bdd 100644 --- a/docs/_static/livecode/livecode.js +++ b/docs/_static/livecode/livecode.js @@ -123,6 +123,29 @@ function keepHighlightedHomepageSource(container, editor, controls, source) { controls.append(edit); } +/** + * @param {Terminal} terminal + * @param {HTMLElement} host + * @returns {() => void} + */ +function fitTerminalWidth(terminal, host) { + /** @type {FitAddon.FitAddon} */ + const addon = new FitAddon.FitAddon(); + terminal.loadAddon(addon); + /** @type {() => void} */ + const fit = () => { + /** @type {{cols: number, rows: number} | undefined} */ + const dimensions = addon.proposeDimensions(); + if (dimensions && dimensions.cols !== terminal.cols) { + terminal.resize(dimensions.cols, terminal.rows); + } + }; + /** @type {ResizeObserver} */ + const observer = new ResizeObserver(fit); + observer.observe(host); + return fit; +} + function createPanel(container, source) { const editor = document.createElement('textarea'); editor.className = 'demo-editor'; @@ -143,6 +166,9 @@ function createPanel(container, source) { const screen = document.createElement('div'); screen.className = 'demo-terminal'; + /** @type {HTMLDivElement} */ + const terminalHost = document.createElement('div'); + screen.append(terminalHost); const controls = document.createElement('div'); controls.className = 'demo-controls'; @@ -153,12 +179,14 @@ function createPanel(container, source) { const terminal = new Terminal({ cols: COLUMNS, - rows: 12, + rows: container.closest('.home-quickstart') ? 3 : 12, convertEol: true, fontSize: 13, theme: {background: '#101418', foreground: '#d6e2ef'}, }); - terminal.open(screen); + terminal.open(terminalHost); + /** @type {() => void} */ + const fitWidth = fitTerminalWidth(terminal, terminalHost); const panel = { terminal, @@ -195,6 +223,8 @@ function createPanel(container, source) { panel.setStatus('idle'); return; } + await new Promise(resolve => requestAnimationFrame(resolve)); + fitWidth(); panel.setStatus('ready'); timeoutHandle = setTimeout(() => { terminal.write('\r\n\x1b[33mStopped after 30 seconds.\x1b[0m\r\n'); @@ -204,7 +234,7 @@ function createPanel(container, source) { worker.postMessage({ type: 'run', code: editor.value, - columns: COLUMNS, + columns: terminal.cols, }); }); @@ -224,7 +254,7 @@ const NON_RUNNABLE_DEMOS = new Set([ ]); document.addEventListener('DOMContentLoaded', () => { - if (typeof Terminal === 'undefined') return; + if (typeof Terminal === 'undefined' || typeof FitAddon === 'undefined') return; for (const container of document.querySelectorAll('.demo-run')) { if (NON_RUNNABLE_DEMOS.has(container.dataset.demo)) { container.textContent = diff --git a/docs/_static/vendor/LICENSE-addon-fit.txt b/docs/_static/vendor/LICENSE-addon-fit.txt new file mode 100644 index 00000000..8f178925 --- /dev/null +++ b/docs/_static/vendor/LICENSE-addon-fit.txt @@ -0,0 +1,19 @@ +Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/docs/_static/vendor/README.md b/docs/_static/vendor/README.md index cb12d3aa..c20840ae 100644 --- a/docs/_static/vendor/README.md +++ b/docs/_static/vendor/README.md @@ -3,6 +3,7 @@ | File | Source | Version | License | | --- | --- | --- | --- | | `xterm.js`, `xterm.css` | `@xterm/xterm` | 5.5.0 | MIT (`LICENSE-xterm.txt`) | +| `addon-fit.js`, `addon-fit.js.map` | `@xterm/addon-fit` | 0.10.0 | MIT (`LICENSE-addon-fit.txt`) | Update (bump the version in both commands and this table): @@ -14,3 +15,13 @@ cp package/css/xterm.css docs/_static/vendor/xterm.css cp package/LICENSE docs/_static/vendor/LICENSE-xterm.txt rm -rf package xterm-xterm-5.5.0.tgz ``` + +Update the matching terminal sizing addon: + +```bash +npm pack @xterm/addon-fit@0.10.0 +tar -xzf xterm-addon-fit-0.10.0.tgz +cp package/lib/addon-fit.js package/lib/addon-fit.js.map docs/_static/vendor/ +cp package/LICENSE docs/_static/vendor/LICENSE-addon-fit.txt +rm -rf package xterm-addon-fit-0.10.0.tgz +``` diff --git a/docs/_static/vendor/addon-fit.js b/docs/_static/vendor/addon-fit.js new file mode 100644 index 00000000..1ccffe8b --- /dev/null +++ b/docs/_static/vendor/addon-fit.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})())); +//# sourceMappingURL=addon-fit.js.map diff --git a/docs/_static/vendor/addon-fit.js.map b/docs/_static/vendor/addon-fit.js.map new file mode 100644 index 00000000..36d6b494 --- /dev/null +++ b/docs/_static/vendor/addon-fit.js.map @@ -0,0 +1 @@ +{"version":3,"file":"addon-fit.js","mappings":"CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAkB,SAAID,IAEtBD,EAAe,SAAIC,GACpB,CATD,CASGK,MAAM,I,mHCeT,iBAGS,QAAAC,CAASC,GACdC,KAAKC,UAAYF,CACnB,CAEO,OAAAG,GAAiB,CAEjB,GAAAC,GACL,MAAMC,EAAOJ,KAAKK,oBAClB,IAAKD,IAASJ,KAAKC,WAAaK,MAAMF,EAAKG,OAASD,MAAMF,EAAKI,MAC7D,OAIF,MAAMC,EAAQT,KAAKC,UAAkBS,MAGjCV,KAAKC,UAAUO,OAASJ,EAAKI,MAAQR,KAAKC,UAAUM,OAASH,EAAKG,OACpEE,EAAKE,eAAeC,QACpBZ,KAAKC,UAAUY,OAAOT,EAAKG,KAAMH,EAAKI,MAE1C,CAEO,iBAAAH,GACL,IAAKL,KAAKC,UACR,OAGF,IAAKD,KAAKC,UAAUa,UAAYd,KAAKC,UAAUa,QAAQC,cACrD,OAIF,MAAMN,EAAQT,KAAKC,UAAkBS,MAC/BN,EAA0BK,EAAKE,eAAeK,WAEpD,GAA4B,IAAxBZ,EAAKa,IAAIC,KAAKC,OAAwC,IAAzBf,EAAKa,IAAIC,KAAKE,OAC7C,OAGF,MAAMC,EAAuD,IAAtCrB,KAAKC,UAAUqB,QAAQC,WAC5C,EAAId,EAAKe,SAASC,eAEdC,EAAqBC,OAAOC,iBAAiB5B,KAAKC,UAAUa,QAAQC,eACpEc,EAAsBC,SAASJ,EAAmBK,iBAAiB,WACnEC,EAAqBC,KAAKC,IAAI,EAAGJ,SAASJ,EAAmBK,iBAAiB,WAC9EI,EAAeR,OAAOC,iBAAiB5B,KAAKC,UAAUa,SAStDsB,EAAkBP,GAPjBC,SAASK,EAAaJ,iBAAiB,gBACpCD,SAASK,EAAaJ,iBAAiB,oBAO3CM,EAAiBL,GANdF,SAASK,EAAaJ,iBAAiB,kBACxCD,SAASK,EAAaJ,iBAAiB,kBAKiBV,EAKhE,MAJiB,CACfd,KAAM0B,KAAKC,IA/DI,EA+DcD,KAAKK,MAAMD,EAAiBjC,EAAKa,IAAIC,KAAKC,QACvEX,KAAMyB,KAAKC,IA/DI,EA+DcD,KAAKK,MAAMF,EAAkBhC,EAAKa,IAAIC,KAAKE,SAG5E,E","sources":["webpack://FitAddon/webpack/universalModuleDefinition","webpack://FitAddon/./src/FitAddon.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"FitAddon\"] = factory();\n\telse\n\t\troot[\"FitAddon\"] = factory();\n})(self, () => {\nreturn ","/**\n * Copyright (c) 2017 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport type { Terminal, ITerminalAddon } from '@xterm/xterm';\nimport type { FitAddon as IFitApi } from '@xterm/addon-fit';\nimport { IRenderDimensions } from 'browser/renderer/shared/Types';\n\ninterface ITerminalDimensions {\n /**\n * The number of rows in the terminal.\n */\n rows: number;\n\n /**\n * The number of columns in the terminal.\n */\n cols: number;\n}\n\nconst MINIMUM_COLS = 2;\nconst MINIMUM_ROWS = 1;\n\nexport class FitAddon implements ITerminalAddon , IFitApi {\n private _terminal: Terminal | undefined;\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n }\n\n public dispose(): void {}\n\n public fit(): void {\n const dims = this.proposeDimensions();\n if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {\n return;\n }\n\n // TODO: Remove reliance on private API\n const core = (this._terminal as any)._core;\n\n // Force a full render\n if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {\n core._renderService.clear();\n this._terminal.resize(dims.cols, dims.rows);\n }\n }\n\n public proposeDimensions(): ITerminalDimensions | undefined {\n if (!this._terminal) {\n return undefined;\n }\n\n if (!this._terminal.element || !this._terminal.element.parentElement) {\n return undefined;\n }\n\n // TODO: Remove reliance on private API\n const core = (this._terminal as any)._core;\n const dims: IRenderDimensions = core._renderService.dimensions;\n\n if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {\n return undefined;\n }\n\n const scrollbarWidth = this._terminal.options.scrollback === 0 ?\n 0 : core.viewport.scrollBarWidth;\n\n const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);\n const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));\n const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));\n const elementStyle = window.getComputedStyle(this._terminal.element);\n const elementPadding = {\n top: parseInt(elementStyle.getPropertyValue('padding-top')),\n bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),\n right: parseInt(elementStyle.getPropertyValue('padding-right')),\n left: parseInt(elementStyle.getPropertyValue('padding-left'))\n };\n const elementPaddingVer = elementPadding.top + elementPadding.bottom;\n const elementPaddingHor = elementPadding.right + elementPadding.left;\n const availableHeight = parentElementHeight - elementPaddingVer;\n const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;\n const geometry = {\n cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),\n rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))\n };\n return geometry;\n }\n}\n"],"names":["root","factory","exports","module","define","amd","self","activate","terminal","this","_terminal","dispose","fit","dims","proposeDimensions","isNaN","cols","rows","core","_core","_renderService","clear","resize","element","parentElement","dimensions","css","cell","width","height","scrollbarWidth","options","scrollback","viewport","scrollBarWidth","parentElementStyle","window","getComputedStyle","parentElementHeight","parseInt","getPropertyValue","parentElementWidth","Math","max","elementStyle","availableHeight","availableWidth","floor"],"sourceRoot":""} diff --git a/docs/examples/_registry.py b/docs/examples/_registry.py index 3bc9f343..0086f961 100644 --- a/docs/examples/_registry.py +++ b/docs/examples/_registry.py @@ -57,6 +57,9 @@ def svg_path(self) -> pathlib.Path: DEMOS: tuple[Demo, ...] = ( + Demo( + 'homepage/quickstart', 'A percentage and a coloured bar', term_width=60 + ), Demo('howto/colors', 'Fixed and gradient bar colors'), Demo('howto/custom-widget', 'The current job phase', term_width=60), Demo( diff --git a/docs/examples/homepage/__init__.py b/docs/examples/homepage/__init__.py new file mode 100644 index 00000000..db0cb006 --- /dev/null +++ b/docs/examples/homepage/__init__.py @@ -0,0 +1 @@ +"""Examples for the documentation homepage.""" diff --git a/docs/examples/homepage/quickstart.py b/docs/examples/homepage/quickstart.py new file mode 100644 index 00000000..82f244d1 --- /dev/null +++ b/docs/examples/homepage/quickstart.py @@ -0,0 +1,17 @@ +"""Show a percentage and a bar that changes colour as work progresses.""" + +import time + +import progressbar + + +def main() -> None: + for _ in progressbar.progressbar( + range(100), + widgets=[progressbar.Percentage(), ' ', progressbar.Bar()], + ): + time.sleep(0.03) + + +if __name__ == '__main__': + main() diff --git a/docs/index.rst b/docs/index.rst index 52f73b94..899ba7c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -74,7 +74,7 @@ progressbar2

Try a complete example

Run this example in your browser. Python downloads when you press Run.

- .. demo:: tutorial/step1 + .. demo:: homepage/quickstart .. raw:: html @@ -95,7 +95,7 @@ progressbar2 Wrap an iterable to show its progress: - .. demo:: tutorial/step1 + .. demo:: homepage/quickstart The :doc:`tutorial/index` builds from this loop to custom widgets. :doc:`howto/index` covers file transfers, several bars at once and printing diff --git a/tests/console/test_homepage.py b/tests/console/test_homepage.py index 961f78a9..119e03ec 100644 --- a/tests/console/test_homepage.py +++ b/tests/console/test_homepage.py @@ -100,6 +100,45 @@ def test_homepage_can_edit_run_and_reset_the_example( assert not page[1] +def test_homepage_output_has_colour_and_fits_after_resizing( + server: str, + page: tuple[Page, list[str]], +) -> None: + browser_page: Page = page[0] + browser_page.goto(f'{server}/index.html') + width: int + for width in (1440, 768, 375): + browser_page.set_viewport_size({'width': width, 'height': 1000}) + browser_page.get_by_role('button', name='Run', exact=True).click() + _wait_for_terminal_text(browser_page, '100%', BOOT_TIMEOUT_MS) + browser_page.wait_for_function("""() => { + const term = window.__consoleTestTerminal; + const line = term.buffer.active.getLine(0); + return [...Array(term.cols).keys()].some(x => { + const cell = line.getCell(x); + return cell.getChars() === '#' && !cell.isFgDefault(); + }); + }""") + geometry: dict[str, float | str] = browser_page.locator( + '.home-quickstart .demo-terminal' + ).evaluate("""panel => { + const viewport = panel.querySelector('.xterm-viewport'); + return { + width: panel.clientWidth, + scrollWidth: panel.scrollWidth, + height: panel.clientHeight, + viewportHeight: viewport.clientHeight, + scrollHeight: viewport.scrollHeight, + overflowY: getComputedStyle(viewport).overflowY, + }; + }""") + assert geometry['scrollWidth'] <= geometry['width'] + assert geometry['scrollHeight'] <= geometry['viewportHeight'] + assert geometry['height'] < 100 + assert geometry['overflowY'] == 'auto' + assert not page[1] + + def test_showcase_keyboard_updates_recording_title_and_guide( server: str, page: tuple[Page, list[str]], @@ -203,7 +242,7 @@ def test_homepage_source_failure_keeps_readable_example( page: tuple[Page, list[str]], ) -> None: browser_page: Page = page[0] - browser_page.route('**/_static/examples/tutorial-step1.py', _missing) + browser_page.route('**/_static/examples/homepage-quickstart.py', _missing) browser_page.goto(f'{server}/index.html') playwright_api.expect( browser_page.locator('.home-quickstart .demo-run') From b753a3c3e2d14ed073af65fafd7b24949bdfad61 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 14 Sep 2026 02:54:18 +0200 Subject: [PATCH 13/18] Unify runnable code views and use coloured tutorial bars --- docs/_static/demos/howto-iterable-wrapper.svg | 2 +- docs/_static/demos/tutorial-step1.svg | 2 +- docs/_static/home.css | 7 +-- docs/_static/livecode/livecode.css | 25 +++++++++++ docs/_static/livecode/livecode.js | 11 +++-- docs/examples/howto/iterable_wrapper.py | 2 +- docs/examples/tutorial/step1.py | 2 +- docs/howto/iterable-wrapper.rst | 3 ++ docs/tutorial/step1.rst | 3 ++ tests/console/test_homepage.py | 44 +++++++++++++++++++ 10 files changed, 85 insertions(+), 16 deletions(-) diff --git a/docs/_static/demos/howto-iterable-wrapper.svg b/docs/_static/demos/howto-iterable-wrapper.svg index 17fb546e..7220a67f 100644 --- a/docs/_static/demos/howto-iterable-wrapper.svg +++ b/docs/_static/demos/howto-iterable-wrapper.svg @@ -56,5 +56,5 @@ Wrapping an iterable directly - 0% (0 of 24) | | Elapsed Time: 0:00:00 ETA: --:--:--4% (1 of 24) |## | Elapsed Time: 0:00:00 ETA: 0:00:008% (2 of 24) |#### | Elapsed Time: 0:00:00 ETA: 0:00:0012% (3 of 24) |####### | Elapsed Time: 0:00:00 ETA: 0:00:0017% (4 of 24) |######### | Elapsed Time: 0:00:00 ETA: 0:00:0021% (5 of 24) |############ | Elapsed Time: 0:00:00 ETA: 0:00:0025% (6 of 24) |############## | Elapsed Time: 0:00:00 ETA: 0:00:0029% (7 of 24) |################# | Elapsed Time: 0:00:00 ETA: 0:00:0033% (8 of 24) |################### | Elapsed Time: 0:00:00 ETA: 0:00:0038% (9 of 24) |###################### | Elapsed Time: 0:00:00 ETA: 0:00:0042% (10 of 24) |######################## | Elapsed Time: 0:00:00 ETA: 0:00:0046% (11 of 24) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:0050% (12 of 24) |############################# | Elapsed Time: 0:00:00 ETA: 0:00:0054% (13 of 24) |############################### | Elapsed Time: 0:00:00 ETA: 0:00:0058% (14 of 24) |################################# | Elapsed Time: 0:00:00 ETA: 0:00:0062% (15 of 24) |#################################### | Elapsed Time: 0:00:00 ETA: 0:00:0067% (16 of 24) |###################################### | Elapsed Time: 0:00:00 ETA: 0:00:0071% (17 of 24) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:0075% (18 of 24) |########################################### | Elapsed Time: 0:00:00 ETA: 0:00:0079% (19 of 24) |############################################# | Elapsed Time: 0:00:00 ETA: 0:00:0083% (20 of 24) |################################################ | Elapsed Time: 0:00:00 ETA: 0:00:0088% (21 of 24) |################################################## | Elapsed Time: 0:00:00 ETA: 0:00:0092% (22 of 24) |##################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0096% (23 of 24) |####################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00100% (24 of 24) |##########################################################| Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 0% (0 of 24) | | Elapsed Time: 0:00:00 ETA: --:--:--Second pass: 4% (1 of 24) |# | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 8% (2 of 24) |### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 12% (3 of 24) |##### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 16% (4 of 24) |####### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 20% (5 of 24) |######### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 25% (6 of 24) |########### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 29% (7 of 24) |############ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 33% (8 of 24) |############## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 37% (9 of 24) |################ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 41% (10 of 24) |################# | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 45% (11 of 24) |################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 50% (12 of 24) |##################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 54% (13 of 24) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 58% (14 of 24) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 62% (15 of 24) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 66% (16 of 24) |############################ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 70% (17 of 24) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 75% (18 of 24) |################################ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 79% (19 of 24) |################################## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 83% (20 of 24) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 87% (21 of 24) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 91% (22 of 24) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 95% (23 of 24) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 100% (24 of 24) |###########################################| Elapsed Time: 0:00:00 Time: 0:00:00 + 0% (0 of 24) | | Elapsed Time: 0:00:00 ETA: --:--:-- 4% (1 of 24) |## | Elapsed Time: 0:00:00 ETA: 0:00:00 8% (2 of 24) |#### | Elapsed Time: 0:00:00 ETA: 0:00:00 12% (3 of 24) |####### | Elapsed Time: 0:00:00 ETA: 0:00:00 16% (4 of 24) |######### | Elapsed Time: 0:00:00 ETA: 0:00:00 20% (5 of 24) |########### | Elapsed Time: 0:00:00 ETA: 0:00:00 25% (6 of 24) |############## | Elapsed Time: 0:00:00 ETA: 0:00:00 29% (7 of 24) |################ | Elapsed Time: 0:00:00 ETA: 0:00:00 33% (8 of 24) |################### | Elapsed Time: 0:00:00 ETA: 0:00:00 37% (9 of 24) |##################### | Elapsed Time: 0:00:00 ETA: 0:00:00 41% (10 of 24) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:00 45% (11 of 24) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:00 50% (12 of 24) |############################ | Elapsed Time: 0:00:00 ETA: 0:00:00 54% (13 of 24) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:00 58% (14 of 24) |################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 62% (15 of 24) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 66% (16 of 24) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 70% (17 of 24) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 75% (18 of 24) |########################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 79% (19 of 24) |############################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 83% (20 of 24) |############################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 87% (21 of 24) |################################################# | Elapsed Time: 0:00:00 ETA: 0:00:00 91% (22 of 24) |################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 95% (23 of 24) |##################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00100% (24 of 24) |########################################################| Elapsed Time: 0:00:00 Time: 0:00:00Second pass: 0% (0 of 24) | | Elapsed Time: 0:00:00 ETA: --:--:--Second pass: 4% (1 of 24) |# | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 8% (2 of 24) |### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 12% (3 of 24) |##### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 16% (4 of 24) |####### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 20% (5 of 24) |######### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 25% (6 of 24) |########### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 29% (7 of 24) |############ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 33% (8 of 24) |############## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 37% (9 of 24) |################ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 41% (10 of 24) |################# | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 45% (11 of 24) |################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 50% (12 of 24) |##################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 54% (13 of 24) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 58% (14 of 24) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 62% (15 of 24) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 66% (16 of 24) |############################ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 70% (17 of 24) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 75% (18 of 24) |################################ | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 79% (19 of 24) |################################## | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 83% (20 of 24) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 87% (21 of 24) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 91% (22 of 24) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 95% (23 of 24) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:00Second pass: 100% (24 of 24) |###########################################| Elapsed Time: 0:00:00 Time: 0:00:00 diff --git a/docs/_static/demos/tutorial-step1.svg b/docs/_static/demos/tutorial-step1.svg index 1e49679f..99380475 100644 --- a/docs/_static/demos/tutorial-step1.svg +++ b/docs/_static/demos/tutorial-step1.svg @@ -56,5 +56,5 @@ Wrap an iterable - 0% (0 of 100) | | Elapsed Time: 0:00:00 ETA: --:--:--1% (1 of 100) | | Elapsed Time: 0:00:00 ETA: 0:00:002% (2 of 100) |# | Elapsed Time: 0:00:00 ETA: 0:00:003% (3 of 100) |# | Elapsed Time: 0:00:00 ETA: 0:00:004% (4 of 100) |## | Elapsed Time: 0:00:00 ETA: 0:00:005% (5 of 100) |## | Elapsed Time: 0:00:00 ETA: 0:00:006% (6 of 100) |### | Elapsed Time: 0:00:00 ETA: 0:00:007% (7 of 100) |#### | Elapsed Time: 0:00:00 ETA: 0:00:008% (8 of 100) |#### | Elapsed Time: 0:00:00 ETA: 0:00:009% (9 of 100) |##### | Elapsed Time: 0:00:00 ETA: 0:00:0010% (10 of 100) |##### | Elapsed Time: 0:00:00 ETA: 0:00:0011% (11 of 100) |###### | Elapsed Time: 0:00:00 ETA: 0:00:0012% (12 of 100) |###### | Elapsed Time: 0:00:00 ETA: 0:00:0013% (13 of 100) |####### | Elapsed Time: 0:00:00 ETA: 0:00:0014% (14 of 100) |####### | Elapsed Time: 0:00:00 ETA: 0:00:0015% (15 of 100) |######## | Elapsed Time: 0:00:00 ETA: 0:00:0016% (16 of 100) |######### | Elapsed Time: 0:00:00 ETA: 0:00:0017% (17 of 100) |######### | Elapsed Time: 0:00:00 ETA: 0:00:0018% (18 of 100) |########## | Elapsed Time: 0:00:00 ETA: 0:00:0019% (19 of 100) |########## | Elapsed Time: 0:00:00 ETA: 0:00:0020% (20 of 100) |########### | Elapsed Time: 0:00:00 ETA: 0:00:0021% (21 of 100) |########### | Elapsed Time: 0:00:00 ETA: 0:00:0022% (22 of 100) |############ | Elapsed Time: 0:00:00 ETA: 0:00:0023% (23 of 100) |############# | Elapsed Time: 0:00:00 ETA: 0:00:0024% (24 of 100) |############# | Elapsed Time: 0:00:00 ETA: 0:00:0025% (25 of 100) |############## | Elapsed Time: 0:00:00 ETA: 0:00:0026% (26 of 100) |############## | Elapsed Time: 0:00:00 ETA: 0:00:0027% (27 of 100) |############### | Elapsed Time: 0:00:00 ETA: 0:00:0028% (28 of 100) |############### | Elapsed Time: 0:00:00 ETA: 0:00:0029% (29 of 100) |################ | Elapsed Time: 0:00:00 ETA: 0:00:0030% (30 of 100) |################# | Elapsed Time: 0:00:00 ETA: 0:00:0031% (31 of 100) |################# | Elapsed Time: 0:00:00 ETA: 0:00:0032% (32 of 100) |################## | Elapsed Time: 0:00:00 ETA: 0:00:0033% (33 of 100) |################## | Elapsed Time: 0:00:00 ETA: 0:00:0034% (34 of 100) |################### | Elapsed Time: 0:00:00 ETA: 0:00:0035% (35 of 100) |################### | Elapsed Time: 0:00:00 ETA: 0:00:0036% (36 of 100) |#################### | Elapsed Time: 0:00:00 ETA: 0:00:0037% (37 of 100) |##################### | Elapsed Time: 0:00:00 ETA: 0:00:0038% (38 of 100) |##################### | Elapsed Time: 0:00:00 ETA: 0:00:0039% (39 of 100) |###################### | Elapsed Time: 0:00:00 ETA: 0:00:0040% (40 of 100) |###################### | Elapsed Time: 0:00:00 ETA: 0:00:0041% (41 of 100) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:0042% (42 of 100) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:0043% (43 of 100) |######################## | Elapsed Time: 0:00:00 ETA: 0:00:0044% (44 of 100) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:0045% (45 of 100) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:0046% (46 of 100) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:0047% (47 of 100) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:0048% (48 of 100) |########################### | Elapsed Time: 0:00:00 ETA: 0:00:0049% (49 of 100) |########################### | Elapsed Time: 0:00:00 ETA: 0:00:0050% (50 of 100) |############################ | Elapsed Time: 0:00:00 ETA: 0:00:0051% (51 of 100) |############################# | Elapsed Time: 0:00:00 ETA: 0:00:0052% (52 of 100) |############################# | Elapsed Time: 0:00:00 ETA: 0:00:0053% (53 of 100) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:0054% (54 of 100) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:0055% (55 of 100) |############################### | Elapsed Time: 0:00:00 ETA: 0:00:0056% (56 of 100) |############################### | Elapsed Time: 0:00:00 ETA: 0:00:0057% (57 of 100) |################################ | Elapsed Time: 0:00:00 ETA: 0:00:0058% (58 of 100) |################################# | Elapsed Time: 0:00:00 ETA: 0:00:0059% (59 of 100) |################################# | Elapsed Time: 0:00:00 ETA: 0:00:0060% (60 of 100) |################################## | Elapsed Time: 0:00:00 ETA: 0:00:0061% (61 of 100) |################################## | Elapsed Time: 0:00:00 ETA: 0:00:0062% (62 of 100) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:0063% (63 of 100) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:0064% (64 of 100) |#################################### | Elapsed Time: 0:00:00 ETA: 0:00:0065% (65 of 100) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:0066% (66 of 100) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:0067% (67 of 100) |###################################### | Elapsed Time: 0:00:00 ETA: 0:00:0068% (68 of 100) |###################################### | Elapsed Time: 0:00:00 ETA: 0:00:0069% (69 of 100) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:0070% (70 of 100) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:0071% (71 of 100) |######################################## | Elapsed Time: 0:00:00 ETA: 0:00:0072% (72 of 100) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:0073% (73 of 100) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:0074% (74 of 100) |########################################## | Elapsed Time: 0:00:00 ETA: 0:00:0075% (75 of 100) |########################################## | Elapsed Time: 0:00:00 ETA: 0:00:0076% (76 of 100) |########################################### | Elapsed Time: 0:00:00 ETA: 0:00:0077% (77 of 100) |########################################### | Elapsed Time: 0:00:00 ETA: 0:00:0078% (78 of 100) |############################################ | Elapsed Time: 0:00:00 ETA: 0:00:0079% (79 of 100) |############################################# | Elapsed Time: 0:00:00 ETA: 0:00:0080% (80 of 100) |############################################# | Elapsed Time: 0:00:00 ETA: 0:00:0081% (81 of 100) |############################################## | Elapsed Time: 0:00:00 ETA: 0:00:0082% (82 of 100) |############################################## | Elapsed Time: 0:00:00 ETA: 0:00:0083% (83 of 100) |############################################### | Elapsed Time: 0:00:00 ETA: 0:00:0084% (84 of 100) |############################################### | Elapsed Time: 0:00:00 ETA: 0:00:0085% (85 of 100) |################################################ | Elapsed Time: 0:00:00 ETA: 0:00:0086% (86 of 100) |################################################# | Elapsed Time: 0:00:00 ETA: 0:00:0087% (87 of 100) |################################################# | Elapsed Time: 0:00:00 ETA: 0:00:0088% (88 of 100) |################################################## | Elapsed Time: 0:00:00 ETA: 0:00:0089% (89 of 100) |################################################## | Elapsed Time: 0:00:00 ETA: 0:00:0090% (90 of 100) |################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0091% (91 of 100) |################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0092% (92 of 100) |#################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0093% (93 of 100) |##################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0094% (94 of 100) |##################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0095% (95 of 100) |###################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0096% (96 of 100) |###################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0097% (97 of 100) |####################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0098% (98 of 100) |####################################################### | Elapsed Time: 0:00:00 ETA: 0:00:0099% (99 of 100) |######################################################## | Elapsed Time: 0:00:00 ETA: 0:00:00100% (100 of 100) |########################################################| Elapsed Time: 0:00:01 ETA: 0:00:00 + 0% (0 of 100) | | Elapsed Time: 0:00:00 ETA: --:--:-- 1% (1 of 100) | | Elapsed Time: 0:00:00 ETA: 0:00:01 2% (2 of 100) |# | Elapsed Time: 0:00:00 ETA: 0:00:01 3% (3 of 100) |# | Elapsed Time: 0:00:00 ETA: 0:00:01 4% (4 of 100) |## | Elapsed Time: 0:00:00 ETA: 0:00:01 5% (5 of 100) |## | Elapsed Time: 0:00:00 ETA: 0:00:01 6% (6 of 100) |### | Elapsed Time: 0:00:00 ETA: 0:00:01 7% (7 of 100) |### | Elapsed Time: 0:00:00 ETA: 0:00:01 8% (8 of 100) |#### | Elapsed Time: 0:00:00 ETA: 0:00:01 9% (9 of 100) |##### | Elapsed Time: 0:00:00 ETA: 0:00:01 10% (10 of 100) |##### | Elapsed Time: 0:00:00 ETA: 0:00:00 11% (11 of 100) |###### | Elapsed Time: 0:00:00 ETA: 0:00:00 12% (12 of 100) |###### | Elapsed Time: 0:00:00 ETA: 0:00:00 13% (13 of 100) |####### | Elapsed Time: 0:00:00 ETA: 0:00:00 14% (14 of 100) |####### | Elapsed Time: 0:00:00 ETA: 0:00:00 15% (15 of 100) |######## | Elapsed Time: 0:00:00 ETA: 0:00:00 16% (16 of 100) |######## | Elapsed Time: 0:00:00 ETA: 0:00:00 17% (17 of 100) |######### | Elapsed Time: 0:00:00 ETA: 0:00:00 18% (18 of 100) |######### | Elapsed Time: 0:00:00 ETA: 0:00:00 19% (19 of 100) |########## | Elapsed Time: 0:00:00 ETA: 0:00:00 20% (20 of 100) |########### | Elapsed Time: 0:00:00 ETA: 0:00:00 21% (21 of 100) |########### | Elapsed Time: 0:00:00 ETA: 0:00:00 22% (22 of 100) |############ | Elapsed Time: 0:00:00 ETA: 0:00:00 23% (23 of 100) |############ | Elapsed Time: 0:00:00 ETA: 0:00:00 24% (24 of 100) |############# | Elapsed Time: 0:00:00 ETA: 0:00:00 25% (25 of 100) |############# | Elapsed Time: 0:00:00 ETA: 0:00:00 26% (26 of 100) |############## | Elapsed Time: 0:00:00 ETA: 0:00:00 27% (27 of 100) |############## | Elapsed Time: 0:00:00 ETA: 0:00:00 28% (28 of 100) |############### | Elapsed Time: 0:00:00 ETA: 0:00:00 29% (29 of 100) |############### | Elapsed Time: 0:00:00 ETA: 0:00:00 30% (30 of 100) |################ | Elapsed Time: 0:00:00 ETA: 0:00:00 31% (31 of 100) |################# | Elapsed Time: 0:00:00 ETA: 0:00:00 32% (32 of 100) |################# | Elapsed Time: 0:00:00 ETA: 0:00:00 33% (33 of 100) |################## | Elapsed Time: 0:00:00 ETA: 0:00:00 34% (34 of 100) |################## | Elapsed Time: 0:00:00 ETA: 0:00:00 35% (35 of 100) |################### | Elapsed Time: 0:00:00 ETA: 0:00:00 36% (36 of 100) |################### | Elapsed Time: 0:00:00 ETA: 0:00:00 37% (37 of 100) |#################### | Elapsed Time: 0:00:00 ETA: 0:00:00 38% (38 of 100) |#################### | Elapsed Time: 0:00:00 ETA: 0:00:00 39% (39 of 100) |##################### | Elapsed Time: 0:00:00 ETA: 0:00:00 40% (40 of 100) |###################### | Elapsed Time: 0:00:00 ETA: 0:00:00 41% (41 of 100) |###################### | Elapsed Time: 0:00:00 ETA: 0:00:00 42% (42 of 100) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:00 43% (43 of 100) |####################### | Elapsed Time: 0:00:00 ETA: 0:00:00 44% (44 of 100) |######################## | Elapsed Time: 0:00:00 ETA: 0:00:00 45% (45 of 100) |######################## | Elapsed Time: 0:00:00 ETA: 0:00:00 46% (46 of 100) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:00 47% (47 of 100) |######################### | Elapsed Time: 0:00:00 ETA: 0:00:00 48% (48 of 100) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:00 49% (49 of 100) |########################## | Elapsed Time: 0:00:00 ETA: 0:00:00 50% (50 of 100) |########################### | Elapsed Time: 0:00:00 ETA: 0:00:00 51% (51 of 100) |############################ | Elapsed Time: 0:00:00 ETA: 0:00:00 52% (52 of 100) |############################ | Elapsed Time: 0:00:00 ETA: 0:00:00 53% (53 of 100) |############################# | Elapsed Time: 0:00:00 ETA: 0:00:00 54% (54 of 100) |############################# | Elapsed Time: 0:00:00 ETA: 0:00:00 55% (55 of 100) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:00 56% (56 of 100) |############################## | Elapsed Time: 0:00:00 ETA: 0:00:00 57% (57 of 100) |############################### | Elapsed Time: 0:00:00 ETA: 0:00:00 58% (58 of 100) |############################### | Elapsed Time: 0:00:00 ETA: 0:00:00 59% (59 of 100) |################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 60% (60 of 100) |################################# | Elapsed Time: 0:00:00 ETA: 0:00:00 61% (61 of 100) |################################# | Elapsed Time: 0:00:00 ETA: 0:00:00 62% (62 of 100) |################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 63% (63 of 100) |################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 64% (64 of 100) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 65% (65 of 100) |################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 66% (66 of 100) |#################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 67% (67 of 100) |#################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 68% (68 of 100) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 69% (69 of 100) |##################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 70% (70 of 100) |###################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 71% (71 of 100) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 72% (72 of 100) |####################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 73% (73 of 100) |######################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 74% (74 of 100) |######################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 75% (75 of 100) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 76% (76 of 100) |######################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 77% (77 of 100) |########################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 78% (78 of 100) |########################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 79% (79 of 100) |########################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 80% (80 of 100) |############################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 81% (81 of 100) |############################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 82% (82 of 100) |############################################# | Elapsed Time: 0:00:00 ETA: 0:00:00 83% (83 of 100) |############################################# | Elapsed Time: 0:00:00 ETA: 0:00:00 84% (84 of 100) |############################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 85% (85 of 100) |############################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 86% (86 of 100) |############################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 87% (87 of 100) |############################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 88% (88 of 100) |################################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 89% (89 of 100) |################################################ | Elapsed Time: 0:00:00 ETA: 0:00:00 90% (90 of 100) |################################################# | Elapsed Time: 0:00:00 ETA: 0:00:00 91% (91 of 100) |################################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 92% (92 of 100) |################################################## | Elapsed Time: 0:00:00 ETA: 0:00:00 93% (93 of 100) |################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 94% (94 of 100) |################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 95% (95 of 100) |#################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 96% (96 of 100) |#################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 97% (97 of 100) |##################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 98% (98 of 100) |##################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00 99% (99 of 100) |###################################################### | Elapsed Time: 0:00:00 ETA: 0:00:00100% (100 of 100) |######################################################| Elapsed Time: 0:00:01 Time: 0:00:01 diff --git a/docs/_static/home.css b/docs/_static/home.css index 672e373f..a4b47afb 100644 --- a/docs/_static/home.css +++ b/docs/_static/home.css @@ -79,15 +79,10 @@ .home-quickstart .demo-animation { display: none; } .home-quickstart .demo-source { max-height: 330px; overflow: auto; } .home-quickstart .demo-source pre { font-size: 12px; } -.home-quickstart .demo:has(.demo-editor:not([hidden])) .demo-source { display: none; } -.home-quickstart .demo-editor[hidden] { display: none; } .home-quickstart .demo-editor { font-size: 12px; line-height: 1.6; max-height: 400px; } -.home-quickstart .demo-terminal { display: none; } -.home-quickstart .demo-run[data-started] .demo-terminal { display: block; } .home-quickstart .demo-controls { border-top: 0; padding: 11px 14px; } .home-quickstart .demo-button { padding: 6px 16px; font: 13px var(--font-stack); } -.home-quickstart .demo-edit { padding: 6px 12px; font: 13px var(--font-stack); border: 1px solid var(--color-background-border); border-radius: 4px; background: var(--color-background-primary); color: var(--color-foreground-primary); cursor: pointer; } -.home-quickstart .demo-edit:hover { border-color: var(--home-accent); } +.home-quickstart .demo-edit { padding: 6px 12px; font: 13px var(--font-stack); } .home-quickstart .demo-run:not(:has(.demo-editor)) { padding: 12px; font-size: 13px; } .home-guide-links { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 48px 40px 42px; padding-top: 27px; border-top: 1px solid var(--color-background-border); } .home-guide-links a { text-decoration: none; } diff --git a/docs/_static/livecode/livecode.css b/docs/_static/livecode/livecode.css index e2a4f227..2340e2e3 100644 --- a/docs/_static/livecode/livecode.css +++ b/docs/_static/livecode/livecode.css @@ -17,6 +17,11 @@ border-top: 1px solid var(--color-background-border); } +.demo:has(.demo-editor:not([hidden])) .demo-source, +.demo-editor[hidden] { + display: none; +} + .demo .demo-run:empty { display: none; } @@ -51,6 +56,21 @@ cursor: default; } +.demo-edit { + padding: 0.25rem 0.75rem; + font: inherit; + font-size: 0.85em; + border: 1px solid var(--color-background-border); + border-radius: 4px; + background: var(--color-background-primary); + color: var(--color-foreground-primary); + cursor: pointer; +} + +.demo-edit:hover { + border-color: var(--home-accent); +} + .demo-status { font-size: 0.85em; color: var(--color-foreground-secondary); @@ -70,11 +90,16 @@ } .demo-terminal { + display: none; background: #101418; padding: 0.5rem; overflow-x: auto; } +.demo-run[data-started] .demo-terminal { + display: block; +} + .demo-terminal .xterm-viewport { overflow-y: auto; } diff --git a/docs/_static/livecode/livecode.js b/docs/_static/livecode/livecode.js index 0c084bdd..a3b34c01 100644 --- a/docs/_static/livecode/livecode.js +++ b/docs/_static/livecode/livecode.js @@ -20,6 +20,8 @@ let worker = null; let booting = null; let activePanel = null; let timeoutHandle = null; +/** @type {number} */ +let editorCount = 0; function bootWorker() { if (booting) return booting; @@ -92,16 +94,13 @@ function finishRun() { } /** - * @param {HTMLElement} container * @param {HTMLTextAreaElement} editor * @param {HTMLElement} controls * @param {string} source */ -function keepHighlightedHomepageSource(container, editor, controls, source) { - if (!container.closest('.home-quickstart')) return; - +function keepHighlightedSource(editor, controls, source) { editor.hidden = true; - editor.id = 'home-example-editor'; + editor.id = `demo-editor-${++editorCount}`; /** @type {HTMLButtonElement} */ const edit = document.createElement('button'); edit.className = 'demo-edit'; @@ -173,7 +172,7 @@ function createPanel(container, source) { const controls = document.createElement('div'); controls.className = 'demo-controls'; controls.append(button); - keepHighlightedHomepageSource(container, editor, controls, source); + keepHighlightedSource(editor, controls, source); controls.append(status); container.append(controls, editor, screen); diff --git a/docs/examples/howto/iterable_wrapper.py b/docs/examples/howto/iterable_wrapper.py index 05ceea84..abbcb4ee 100644 --- a/docs/examples/howto/iterable_wrapper.py +++ b/docs/examples/howto/iterable_wrapper.py @@ -14,7 +14,7 @@ def main() -> None: - for _ in progressbar.progressbar(range(STEPS)): + for _ in progressbar.progressbar(range(STEPS), fast=False): time.sleep(0.005) bar = progressbar.ProgressBar(prefix='Second pass: ') diff --git a/docs/examples/tutorial/step1.py b/docs/examples/tutorial/step1.py index e635d8b3..1ab782f3 100644 --- a/docs/examples/tutorial/step1.py +++ b/docs/examples/tutorial/step1.py @@ -6,7 +6,7 @@ def main() -> None: - for _ in progressbar.progressbar(range(100)): + for _ in progressbar.progressbar(range(100), fast=False): time.sleep(0.01) diff --git a/docs/howto/iterable-wrapper.rst b/docs/howto/iterable-wrapper.rst index 8ab147bf..ed747d89 100644 --- a/docs/howto/iterable-wrapper.rst +++ b/docs/howto/iterable-wrapper.rst @@ -8,6 +8,9 @@ with a known length. .. demo:: howto/iterable-wrapper +``fast=False`` gives the first pass the same coloured widget renderer as +the explicit ``ProgressBar`` used for the second pass. + ``progressbar.progressbar(iterable)`` wraps any iterable and returns an iterator that updates a fresh bar on every step, sized from ``len(iterable)`` when available. If you already built a ``ProgressBar`` diff --git a/docs/tutorial/step1.rst b/docs/tutorial/step1.rst index 4e17c00c..d8788e14 100644 --- a/docs/tutorial/step1.rst +++ b/docs/tutorial/step1.rst @@ -17,6 +17,9 @@ an iterable, hands back an iterator over the same values, and starts, updates and finishes a bar behind the scenes as that iterator is consumed. There is no separate call to make the bar advance or to mark it done. +``fast=False`` selects the widget renderer with its default colours. +Without it, this simple loop uses the faster, uncoloured renderer. + That convenience comes from hiding the bar object entirely. The library also exposes it directly as the ``ProgressBar`` class, which you construct and update yourself when progress does not come from iterating something. diff --git a/tests/console/test_homepage.py b/tests/console/test_homepage.py index 119e03ec..b330366e 100644 --- a/tests/console/test_homepage.py +++ b/tests/console/test_homepage.py @@ -4,6 +4,8 @@ import typing +import pytest + from .test_console import ( BOOT_TIMEOUT_MS, _wait_for_terminal_text, @@ -139,6 +141,48 @@ def test_homepage_output_has_colour_and_fits_after_resizing( assert not page[1] +@pytest.mark.parametrize( + 'path', + [ + 'tutorial/step1.html', + 'howto/iterable-wrapper.html', + 'widgets/bar.html', + ], +) +def test_guide_example_has_one_code_view_and_coloured_output( + server: str, + page: tuple[Page, list[str]], + path: str, +) -> None: + browser_page: Page = page[0] + browser_page.goto(f'{server}/{path}') + source: Locator = browser_page.locator('.demo-source') + editor: Locator = browser_page.locator('.demo-editor') + playwright_api.expect(editor).to_be_attached() + playwright_api.expect(source).to_be_visible() + playwright_api.expect(editor).to_be_hidden() + browser_page.get_by_role('button', name='Run', exact=True).click() + _wait_for_terminal_text(browser_page, '100%', BOOT_TIMEOUT_MS) + browser_page.wait_for_function( + """() => { + const term = window.__consoleTestTerminal; + const line = term.buffer.active.getLine(0); + return [...Array(term.cols).keys()].some(x => { + const cell = line.getCell(x); + return cell.getChars() === '#' && !cell.isFgDefault(); + }); + }""", + timeout=5000, + ) + browser_page.get_by_role('button', name='Edit code').click() + playwright_api.expect(source).to_be_hidden() + playwright_api.expect(editor).to_be_focused() + browser_page.get_by_role('button', name='Reset example').click() + playwright_api.expect(source).to_be_visible() + playwright_api.expect(editor).to_be_hidden() + assert not page[1] + + def test_showcase_keyboard_updates_recording_title_and_guide( server: str, page: tuple[Page, list[str]], From b52dfef13634facb73699f66c3723934f71b968c Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 14 Sep 2026 03:02:32 +0200 Subject: [PATCH 14/18] Version runnable example URLs to avoid stale cached code --- docs/_ext/demo.py | 3 +++ tests/console/test_homepage.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/_ext/demo.py b/docs/_ext/demo.py index 95616d08..a2689d33 100644 --- a/docs/_ext/demo.py +++ b/docs/_ext/demo.py @@ -7,6 +7,7 @@ from __future__ import annotations +import hashlib import html import importlib.util import pathlib @@ -116,6 +117,8 @@ def run(self) -> list[Node]: here = '' svg_uri = relative_uri(here, f'_static/demos/{demo.svg_path.name}') source_uri = relative_uri(here, f'_static/examples/{stem}.py') + source_hash: str = hashlib.sha256(source.encode('utf-8')).hexdigest() + source_uri += f'?v={source_hash}' container = nodes.container(classes=['demo']) container += nodes.raw( diff --git a/tests/console/test_homepage.py b/tests/console/test_homepage.py index b330366e..d33934c4 100644 --- a/tests/console/test_homepage.py +++ b/tests/console/test_homepage.py @@ -38,6 +38,13 @@ def _stale_home_styles(route: Route) -> None: ) +def _stale_example_source(route: Route) -> None: + route.fulfill( + content_type='text/plain', + body="print('Cached example from an earlier build')", + ) + + def test_homepage_loads_python_only_after_run( server: str, page: tuple[Page, list[str]], @@ -155,12 +162,14 @@ def test_guide_example_has_one_code_view_and_coloured_output( path: str, ) -> None: browser_page: Page = page[0] + browser_page.route('**/_static/examples/*.py', _stale_example_source) browser_page.goto(f'{server}/{path}') source: Locator = browser_page.locator('.demo-source') editor: Locator = browser_page.locator('.demo-editor') playwright_api.expect(editor).to_be_attached() playwright_api.expect(source).to_be_visible() playwright_api.expect(editor).to_be_hidden() + assert editor.input_value().strip() == source.inner_text().strip() browser_page.get_by_role('button', name='Run', exact=True).click() _wait_for_terminal_text(browser_page, '100%', BOOT_TIMEOUT_MS) browser_page.wait_for_function( @@ -286,7 +295,7 @@ def test_homepage_source_failure_keeps_readable_example( page: tuple[Page, list[str]], ) -> None: browser_page: Page = page[0] - browser_page.route('**/_static/examples/homepage-quickstart.py', _missing) + browser_page.route('**/_static/examples/homepage-quickstart.py*', _missing) browser_page.goto(f'{server}/index.html') playwright_api.expect( browser_page.locator('.home-quickstart .demo-run') From b8d82703d120fb88c2ef739dd319b864e5595e93 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 14 Sep 2026 03:17:19 +0200 Subject: [PATCH 15/18] Fix coloured recording checks and point badges at master --- .github/workflows/codeql.yml | 4 ++-- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- README.md | 6 +++--- tests/test_readme_demos.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b634a35f..ee7cae26 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: "CodeQL" on: push: - branches: [ "develop" ] + branches: [ "master", "develop" ] pull_request: - branches: [ "develop" ] + branches: [ "master", "develop" ] schedule: - cron: "24 21 * * 1" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cb6eae7e..89bac84b 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: docs console on: push: - branches: [develop] + branches: [master, develop] pull_request: workflow_dispatch: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be123e86..a677a9f0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,7 +2,7 @@ name: tox on: push: - branches: [develop] + branches: [master, develop] pull_request: workflow_dispatch: diff --git a/README.md b/README.md index 9b411adc..28954c47 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,12 @@ The fastest progress bar in Python, maintained since 2012.

- test status - coverage status + test status + coverage status PyPI version PyPI downloads per month supported Python versions - license + license

Wrapping a fast loop in a progress bar can cost more than the loop diff --git a/tests/test_readme_demos.py b/tests/test_readme_demos.py index 65b459c6..910ed1b3 100644 --- a/tests/test_readme_demos.py +++ b/tests/test_readme_demos.py @@ -576,7 +576,7 @@ def test_tutorial_recording_preserves_intermediate_progress() -> None: int(match.group()[:-1]) for frame in frames for line in frame - if (match := demos.PERCENT_RE.search(line)) + if (match := demos.PERCENT_RE.search(demos.ANSI_SGR_RE.sub('', line))) ] assert percentages[0] == 0 assert percentages[-1] == 100 From a5f421a628a114820bb9c84a3e0c0e5ef08f5656 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 14 Sep 2026 05:04:23 +0200 Subject: [PATCH 16/18] Colour every runnable documentation example and audit the full gallery --- README.md | 2 + docs/_static/demos/howto-unknown-length.svg | 2 +- docs/_static/demos/readme-unknown-length.svg | 2 +- docs/_static/demos/tutorial-step2.svg | 2 +- .../_static/demos/widgets-animated-marker.svg | 2 +- docs/_static/demos/widgets-bouncing-bar.svg | 2 +- docs/_static/demos/widgets-counter.svg | 2 +- docs/_static/demos/widgets-format-label.svg | 2 +- docs/_static/demos/widgets-timer.svg | 2 +- docs/_static/demos/widgets-unit-progress.svg | 2 +- docs/_static/livecode/livecode.js | 3 +- docs/examples/howto/unknown_length.py | 6 +- docs/examples/readme/unknown_length.py | 2 + docs/examples/tutorial/step2.py | 5 +- docs/examples/widgets/animated_marker.py | 9 +- docs/examples/widgets/bouncing_bar.py | 6 +- docs/examples/widgets/counter.py | 7 +- docs/examples/widgets/format_label.py | 3 +- docs/examples/widgets/timer.py | 5 +- docs/examples/widgets/unit_progress.py | 3 +- docs/howto/unknown-length.rst | 3 + docs/tutorial/step2.rst | 5 +- docs/widgets/animated-marker.rst | 3 + docs/widgets/bouncing-bar.rst | 2 + docs/widgets/counter.rst | 2 + docs/widgets/format-label.rst | 2 + docs/widgets/timer.rst | 2 + docs/widgets/unit-progress.rst | 2 + tests/console/test_console.py | 14 +++ tests/console/test_example_colours.py | 97 +++++++++++++++++++ tests/test_readme_demos.py | 6 +- 31 files changed, 187 insertions(+), 20 deletions(-) create mode 100644 tests/console/test_example_colours.py diff --git a/README.md b/README.md index 28954c47..373d7eb8 100644 --- a/README.md +++ b/README.md @@ -350,11 +350,13 @@ with a counter instead of a percentage: import time import progressbar +from progressbar.terminal import colors def main() -> None: with progressbar.ProgressBar( max_value=progressbar.UnknownLength, + widget_kwargs={'marker_wrap': colors.cyan1.fg('{}')}, ) as bar: for value in range(0, 120, 10): bar.update(value) diff --git a/docs/_static/demos/howto-unknown-length.svg b/docs/_static/demos/howto-unknown-length.svg index be199266..f02f0742 100644 --- a/docs/_static/demos/howto-unknown-length.svg +++ b/docs/_static/demos/howto-unknown-length.svg @@ -56,5 +56,5 @@ UnknownLength with an animated marker - Scanning: / 0 files foundScanning: - 1 files foundScanning: \ 2 files foundScanning: | 3 files foundScanning: / 4 files foundScanning: - 5 files foundScanning: \ 6 files foundScanning: | 7 files foundScanning: / 8 files foundScanning: - 9 files foundScanning: \ 10 files foundScanning: | 11 files foundScanning: / 12 files foundScanning: - 13 files foundScanning: \ 14 files foundScanning: | 15 files foundScanning: / 16 files foundScanning: - 17 files foundScanning: \ 18 files foundScanning: | 19 files foundScanning: / 20 files foundScanning: - 21 files foundScanning: \ 22 files foundScanning: | 23 files foundScanning: / 24 files foundScanning: | 24 files found + Scanning: / 0 files foundScanning: - 1 files foundScanning: \ 2 files foundScanning: | 3 files foundScanning: / 4 files foundScanning: - 5 files foundScanning: \ 6 files foundScanning: | 7 files foundScanning: / 8 files foundScanning: - 9 files foundScanning: \ 10 files foundScanning: | 11 files foundScanning: / 12 files foundScanning: - 13 files foundScanning: \ 14 files foundScanning: | 15 files foundScanning: / 16 files foundScanning: - 17 files foundScanning: \ 18 files foundScanning: | 19 files foundScanning: / 20 files foundScanning: - 21 files foundScanning: \ 22 files foundScanning: | 23 files foundScanning: / 24 files foundScanning: | 24 files found diff --git a/docs/_static/demos/readme-unknown-length.svg b/docs/_static/demos/readme-unknown-length.svg index 93757af6..21bfa96c 100644 --- a/docs/_static/demos/readme-unknown-length.svg +++ b/docs/_static/demos/readme-unknown-length.svg @@ -56,5 +56,5 @@ Unknown length - / |# | 0 Elapsed Time: 0:00:00- |# | 10 Elapsed Time: 0:00:00\ | # | 20 Elapsed Time: 0:00:00| | # | 30 Elapsed Time: 0:00:00/ | # | 40 Elapsed Time: 0:00:00- | # | 50 Elapsed Time: 0:00:00\ | # | 60 Elapsed Time: 0:00:00| | # | 70 Elapsed Time: 0:00:00/ | # | 80 Elapsed Time: 0:00:00- | # | 90 Elapsed Time: 0:00:00\ | # | 100 Elapsed Time: 0:00:01| | # | 110 Elapsed Time: 0:00:01 + / |# | 0 Elapsed Time: 0:00:00- |# | 10 Elapsed Time: 0:00:00\ | # | 20 Elapsed Time: 0:00:00| | # | 30 Elapsed Time: 0:00:00/ | # | 40 Elapsed Time: 0:00:00- | # | 50 Elapsed Time: 0:00:00\ | # | 60 Elapsed Time: 0:00:00| | # | 70 Elapsed Time: 0:00:00/ | # | 80 Elapsed Time: 0:00:00- | # | 90 Elapsed Time: 0:00:00\ | # | 100 Elapsed Time: 0:00:01| | # | 110 Elapsed Time: 0:00:01| | # | 110 Elapsed Time: 0:00:01 diff --git a/docs/_static/demos/tutorial-step2.svg b/docs/_static/demos/tutorial-step2.svg index 4f9fd613..9794abfa 100644 --- a/docs/_static/demos/tutorial-step2.svg +++ b/docs/_static/demos/tutorial-step2.svg @@ -56,5 +56,5 @@ Explicit update() - / |# | 0 Elapsed Time: 0:00:00- |# | 1 Elapsed Time: 0:00:00\ |# | 2 Elapsed Time: 0:00:00| |# | 3 Elapsed Time: 0:00:00/ |# | 4 Elapsed Time: 0:00:00- |# | 5 Elapsed Time: 0:00:00\ |# | 6 Elapsed Time: 0:00:00| |# | 7 Elapsed Time: 0:00:00/ |# | 8 Elapsed Time: 0:00:00- |# | 9 Elapsed Time: 0:00:00\ |# | 10 Elapsed Time: 0:00:00| |# | 11 Elapsed Time: 0:00:00/ | # | 12 Elapsed Time: 0:00:00- | # | 13 Elapsed Time: 0:00:00\ | # | 14 Elapsed Time: 0:00:00| | # | 15 Elapsed Time: 0:00:00/ | # | 16 Elapsed Time: 0:00:00- | # | 17 Elapsed Time: 0:00:00\ | # | 18 Elapsed Time: 0:00:00| | # | 19 Elapsed Time: 0:00:00/ | # | 20 Elapsed Time: 0:00:00- | # | 21 Elapsed Time: 0:00:00\ | # | 22 Elapsed Time: 0:00:00| | # | 23 Elapsed Time: 0:00:00/ | # | 24 Elapsed Time: 0:00:00- | # | 25 Elapsed Time: 0:00:00\ | # | 26 Elapsed Time: 0:00:00| | # | 27 Elapsed Time: 0:00:00/ | # | 28 Elapsed Time: 0:00:00- | # | 29 Elapsed Time: 0:00:00\ | # | 30 Elapsed Time: 0:00:00| | # | 31 Elapsed Time: 0:00:00/ | # | 32 Elapsed Time: 0:00:00- | # | 33 Elapsed Time: 0:00:00\ | # | 34 Elapsed Time: 0:00:00| | # | 35 Elapsed Time: 0:00:00/ | # | 36 Elapsed Time: 0:00:00- | # | 37 Elapsed Time: 0:00:00\ | # | 38 Elapsed Time: 0:00:00| | # | 39 Elapsed Time: 0:00:00/ | # | 40 Elapsed Time: 0:00:00- | # | 41 Elapsed Time: 0:00:00\ | # | 42 Elapsed Time: 0:00:00| | # | 43 Elapsed Time: 0:00:00/ | # | 44 Elapsed Time: 0:00:00- | # | 45 Elapsed Time: 0:00:00\ | # | 46 Elapsed Time: 0:00:00| | # | 47 Elapsed Time: 0:00:00/ | # | 48 Elapsed Time: 0:00:00- | # | 49 Elapsed Time: 0:00:00\ | # | 50 Elapsed Time: 0:00:00| | # | 51 Elapsed Time: 0:00:00/ | # | 52 Elapsed Time: 0:00:00- | # | 53 Elapsed Time: 0:00:00\ | # | 54 Elapsed Time: 0:00:00| | # | 55 Elapsed Time: 0:00:00/ | # | 56 Elapsed Time: 0:00:00- | # | 57 Elapsed Time: 0:00:00\ | # | 58 Elapsed Time: 0:00:00| | # | 59 Elapsed Time: 0:00:00/ | # | 60 Elapsed Time: 0:00:00- | # | 61 Elapsed Time: 0:00:00\ | # | 62 Elapsed Time: 0:00:00| | # | 63 Elapsed Time: 0:00:00/ | # | 64 Elapsed Time: 0:00:00- | # | 65 Elapsed Time: 0:00:00\ | # | 66 Elapsed Time: 0:00:00| | # | 67 Elapsed Time: 0:00:00/ | # | 68 Elapsed Time: 0:00:00- | # | 69 Elapsed Time: 0:00:00\ | # | 70 Elapsed Time: 0:00:00| | # | 71 Elapsed Time: 0:00:00/ | # | 72 Elapsed Time: 0:00:00- | # | 73 Elapsed Time: 0:00:00\ | # | 74 Elapsed Time: 0:00:00| | # | 75 Elapsed Time: 0:00:00/ | # | 76 Elapsed Time: 0:00:00- | # | 77 Elapsed Time: 0:00:00\ | # | 78 Elapsed Time: 0:00:00| | # | 79 Elapsed Time: 0:00:00/ | # | 80 Elapsed Time: 0:00:00- | # | 81 Elapsed Time: 0:00:00\ | # | 82 Elapsed Time: 0:00:00| | # | 83 Elapsed Time: 0:00:00/ | # | 84 Elapsed Time: 0:00:00- | # | 85 Elapsed Time: 0:00:00\ | # | 86 Elapsed Time: 0:00:00| | # | 87 Elapsed Time: 0:00:00/ | # | 88 Elapsed Time: 0:00:00- | # | 89 Elapsed Time: 0:00:00\ | # | 90 Elapsed Time: 0:00:00| | # | 91 Elapsed Time: 0:00:00/ | # | 92 Elapsed Time: 0:00:00- | # | 93 Elapsed Time: 0:00:00\ | # | 94 Elapsed Time: 0:00:00| | # | 95 Elapsed Time: 0:00:00/ | # | 96 Elapsed Time: 0:00:00- | # | 97 Elapsed Time: 0:00:00\ | # | 98 Elapsed Time: 0:00:00| | # | 99 Elapsed Time: 0:00:00/ | # | 100 Elapsed Time: 0:00:00| | # | 100 Elapsed Time: 0:00:00 + / |# | 0 Elapsed Time: 0:00:00- |# | 1 Elapsed Time: 0:00:00\ |# | 2 Elapsed Time: 0:00:00| |# | 3 Elapsed Time: 0:00:00/ |# | 4 Elapsed Time: 0:00:00- |# | 5 Elapsed Time: 0:00:00\ |# | 6 Elapsed Time: 0:00:00| |# | 7 Elapsed Time: 0:00:00/ |# | 8 Elapsed Time: 0:00:00- |# | 9 Elapsed Time: 0:00:00\ |# | 10 Elapsed Time: 0:00:00| |# | 11 Elapsed Time: 0:00:00/ | # | 12 Elapsed Time: 0:00:00- | # | 13 Elapsed Time: 0:00:00\ | # | 14 Elapsed Time: 0:00:00| | # | 15 Elapsed Time: 0:00:00/ | # | 16 Elapsed Time: 0:00:00- | # | 17 Elapsed Time: 0:00:00\ | # | 18 Elapsed Time: 0:00:00| | # | 19 Elapsed Time: 0:00:00/ | # | 20 Elapsed Time: 0:00:00- | # | 21 Elapsed Time: 0:00:00\ | # | 22 Elapsed Time: 0:00:00| | # | 23 Elapsed Time: 0:00:00/ | # | 24 Elapsed Time: 0:00:00- | # | 25 Elapsed Time: 0:00:00\ | # | 26 Elapsed Time: 0:00:00| | # | 27 Elapsed Time: 0:00:00/ | # | 28 Elapsed Time: 0:00:00- | # | 29 Elapsed Time: 0:00:00\ | # | 30 Elapsed Time: 0:00:00| | # | 31 Elapsed Time: 0:00:00/ | # | 32 Elapsed Time: 0:00:00- | # | 33 Elapsed Time: 0:00:00\ | # | 34 Elapsed Time: 0:00:00| | # | 35 Elapsed Time: 0:00:00/ | # | 36 Elapsed Time: 0:00:00- | # | 37 Elapsed Time: 0:00:00\ | # | 38 Elapsed Time: 0:00:00| | # | 39 Elapsed Time: 0:00:00/ | # | 40 Elapsed Time: 0:00:00- | # | 41 Elapsed Time: 0:00:00\ | # | 42 Elapsed Time: 0:00:00| | # | 43 Elapsed Time: 0:00:00/ | # | 44 Elapsed Time: 0:00:00- | # | 45 Elapsed Time: 0:00:00\ | # | 46 Elapsed Time: 0:00:00| | # | 47 Elapsed Time: 0:00:00/ | # | 48 Elapsed Time: 0:00:00- | # | 49 Elapsed Time: 0:00:00\ | # | 50 Elapsed Time: 0:00:00| | # | 51 Elapsed Time: 0:00:00/ | # | 52 Elapsed Time: 0:00:00- | # | 53 Elapsed Time: 0:00:00\ | # | 54 Elapsed Time: 0:00:00| | # | 55 Elapsed Time: 0:00:00/ | # | 56 Elapsed Time: 0:00:00- | # | 57 Elapsed Time: 0:00:00\ | # | 58 Elapsed Time: 0:00:00| | # | 59 Elapsed Time: 0:00:00/ | # | 60 Elapsed Time: 0:00:00- | # | 61 Elapsed Time: 0:00:00\ | # | 62 Elapsed Time: 0:00:00| | # | 63 Elapsed Time: 0:00:00/ | # | 64 Elapsed Time: 0:00:00- | # | 65 Elapsed Time: 0:00:00\ | # | 66 Elapsed Time: 0:00:00| | # | 67 Elapsed Time: 0:00:00/ | # | 68 Elapsed Time: 0:00:00- | # | 69 Elapsed Time: 0:00:00\ | # | 70 Elapsed Time: 0:00:00| | # | 71 Elapsed Time: 0:00:00/ | # | 72 Elapsed Time: 0:00:00- | # | 73 Elapsed Time: 0:00:00\ | # | 74 Elapsed Time: 0:00:00| | # | 75 Elapsed Time: 0:00:00/ | # | 76 Elapsed Time: 0:00:00- | # | 77 Elapsed Time: 0:00:00\ | # | 78 Elapsed Time: 0:00:00| | # | 79 Elapsed Time: 0:00:00/ | # | 80 Elapsed Time: 0:00:00- | # | 81 Elapsed Time: 0:00:00\ | # | 82 Elapsed Time: 0:00:00| | # | 83 Elapsed Time: 0:00:00/ | # | 84 Elapsed Time: 0:00:00- | # | 85 Elapsed Time: 0:00:00\ | # | 86 Elapsed Time: 0:00:00| | # | 87 Elapsed Time: 0:00:00/ | # | 88 Elapsed Time: 0:00:00- | # | 89 Elapsed Time: 0:00:00\ | # | 90 Elapsed Time: 0:00:00| | # | 91 Elapsed Time: 0:00:00/ | # | 92 Elapsed Time: 0:00:00- | # | 93 Elapsed Time: 0:00:00\ | # | 94 Elapsed Time: 0:00:00| | # | 95 Elapsed Time: 0:00:00/ | # | 96 Elapsed Time: 0:00:00- | # | 97 Elapsed Time: 0:00:00\ | # | 98 Elapsed Time: 0:00:00| | # | 99 Elapsed Time: 0:00:00/ | # | 100 Elapsed Time: 0:00:00| | # | 100 Elapsed Time: 0:00:00 diff --git a/docs/_static/demos/widgets-animated-marker.svg b/docs/_static/demos/widgets-animated-marker.svg index 6f7efb43..213e7a9e 100644 --- a/docs/_static/demos/widgets-animated-marker.svg +++ b/docs/_static/demos/widgets-animated-marker.svg @@ -56,5 +56,5 @@ AnimatedMarker - Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: | + Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: -Working: \Working: |Working: /Working: | diff --git a/docs/_static/demos/widgets-bouncing-bar.svg b/docs/_static/demos/widgets-bouncing-bar.svg index 7a8207c5..e9669659 100644 --- a/docs/_static/demos/widgets-bouncing-bar.svg +++ b/docs/_static/demos/widgets-bouncing-bar.svg @@ -56,5 +56,5 @@ BouncingBar - Working: |# |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | #|Working: | # |Working: | # | + Working: |# |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | # |Working: | #|Working: | # |Working: | # | diff --git a/docs/_static/demos/widgets-counter.svg b/docs/_static/demos/widgets-counter.svg index f3331e3f..afec74f0 100644 --- a/docs/_static/demos/widgets-counter.svg +++ b/docs/_static/demos/widgets-counter.svg @@ -56,5 +56,5 @@ Counter - Processed: 0 linesProcessed: 1 linesProcessed: 2 linesProcessed: 3 linesProcessed: 4 linesProcessed: 5 linesProcessed: 6 linesProcessed: 7 linesProcessed: 8 linesProcessed: 9 linesProcessed: 10 linesProcessed: 11 linesProcessed: 12 linesProcessed: 13 linesProcessed: 14 linesProcessed: 15 linesProcessed: 16 linesProcessed: 17 linesProcessed: 18 linesProcessed: 19 linesProcessed: 20 linesProcessed: 21 linesProcessed: 22 linesProcessed: 23 linesProcessed: 24 lines + Processed: 0 linesProcessed: 1 linesProcessed: 2 linesProcessed: 3 linesProcessed: 4 linesProcessed: 5 linesProcessed: 6 linesProcessed: 7 linesProcessed: 8 linesProcessed: 9 linesProcessed: 10 linesProcessed: 11 linesProcessed: 12 linesProcessed: 13 linesProcessed: 14 linesProcessed: 15 linesProcessed: 16 linesProcessed: 17 linesProcessed: 18 linesProcessed: 19 linesProcessed: 20 linesProcessed: 21 linesProcessed: 22 linesProcessed: 23 linesProcessed: 24 lines diff --git a/docs/_static/demos/widgets-format-label.svg b/docs/_static/demos/widgets-format-label.svg index 59bb6ebe..5268aa24 100644 --- a/docs/_static/demos/widgets-format-label.svg +++ b/docs/_static/demos/widgets-format-label.svg @@ -56,5 +56,5 @@ FormatLabel - Processed: 0 lines (in: 0:00:00)Processed: 1 lines (in: 0:00:00)Processed: 2 lines (in: 0:00:00)Processed: 3 lines (in: 0:00:00)Processed: 4 lines (in: 0:00:00)Processed: 5 lines (in: 0:00:00)Processed: 6 lines (in: 0:00:00)Processed: 7 lines (in: 0:00:00)Processed: 8 lines (in: 0:00:00)Processed: 9 lines (in: 0:00:00)Processed: 10 lines (in: 0:00:00)Processed: 11 lines (in: 0:00:00)Processed: 12 lines (in: 0:00:00)Processed: 13 lines (in: 0:00:00)Processed: 14 lines (in: 0:00:00)Processed: 15 lines (in: 0:00:00)Processed: 16 lines (in: 0:00:00)Processed: 17 lines (in: 0:00:00)Processed: 18 lines (in: 0:00:00)Processed: 19 lines (in: 0:00:00)Processed: 20 lines (in: 0:00:00)Processed: 21 lines (in: 0:00:00)Processed: 22 lines (in: 0:00:00)Processed: 23 lines (in: 0:00:00)Processed: 24 lines (in: 0:00:00) + Processed: 0 lines (in: 0:00:00)Processed: 1 lines (in: 0:00:00)Processed: 2 lines (in: 0:00:00)Processed: 3 lines (in: 0:00:00)Processed: 4 lines (in: 0:00:00)Processed: 5 lines (in: 0:00:00)Processed: 6 lines (in: 0:00:00)Processed: 7 lines (in: 0:00:00)Processed: 8 lines (in: 0:00:00)Processed: 9 lines (in: 0:00:00)Processed: 10 lines (in: 0:00:00)Processed: 11 lines (in: 0:00:00)Processed: 12 lines (in: 0:00:00)Processed: 13 lines (in: 0:00:00)Processed: 14 lines (in: 0:00:00)Processed: 15 lines (in: 0:00:00)Processed: 16 lines (in: 0:00:00)Processed: 17 lines (in: 0:00:00)Processed: 18 lines (in: 0:00:00)Processed: 19 lines (in: 0:00:00)Processed: 20 lines (in: 0:00:00)Processed: 21 lines (in: 0:00:00)Processed: 22 lines (in: 0:00:00)Processed: 23 lines (in: 0:00:00)Processed: 24 lines (in: 0:00:00) diff --git a/docs/_static/demos/widgets-timer.svg b/docs/_static/demos/widgets-timer.svg index 27bce9c1..4e6e465c 100644 --- a/docs/_static/demos/widgets-timer.svg +++ b/docs/_static/demos/widgets-timer.svg @@ -56,5 +56,5 @@ Timer - Elapsed Time: 0:00:00Elapsed Time: 0:00:01Elapsed Time: 0:00:02Elapsed Time: 0:00:03Elapsed Time: 0:00:04 + Elapsed Time: 0:00:00Elapsed Time: 0:00:01Elapsed Time: 0:00:02Elapsed Time: 0:00:03Elapsed Time: 0:00:04 diff --git a/docs/_static/demos/widgets-unit-progress.svg b/docs/_static/demos/widgets-unit-progress.svg index 8417ac6b..d04064a1 100644 --- a/docs/_static/demos/widgets-unit-progress.svg +++ b/docs/_static/demos/widgets-unit-progress.svg @@ -56,5 +56,5 @@ UnitProgress - 0 files of 24 files1 files of 24 files2 files of 24 files3 files of 24 files4 files of 24 files5 files of 24 files6 files of 24 files7 files of 24 files8 files of 24 files9 files of 24 files10 files of 24 files11 files of 24 files12 files of 24 files13 files of 24 files14 files of 24 files15 files of 24 files16 files of 24 files17 files of 24 files18 files of 24 files19 files of 24 files20 files of 24 files21 files of 24 files22 files of 24 files23 files of 24 files24 files of 24 files + 0 files of 24 files1 files of 24 files2 files of 24 files3 files of 24 files4 files of 24 files5 files of 24 files6 files of 24 files7 files of 24 files8 files of 24 files9 files of 24 files10 files of 24 files11 files of 24 files12 files of 24 files13 files of 24 files14 files of 24 files15 files of 24 files16 files of 24 files17 files of 24 files18 files of 24 files19 files of 24 files20 files of 24 files21 files of 24 files22 files of 24 files23 files of 24 files24 files of 24 files diff --git a/docs/_static/livecode/livecode.js b/docs/_static/livecode/livecode.js index a3b34c01..abac9ce8 100644 --- a/docs/_static/livecode/livecode.js +++ b/docs/_static/livecode/livecode.js @@ -135,7 +135,8 @@ function fitTerminalWidth(terminal, host) { const fit = () => { /** @type {{cols: number, rows: number} | undefined} */ const dimensions = addon.proposeDimensions(); - if (dimensions && dimensions.cols !== terminal.cols) { + if (dimensions && Number.isInteger(dimensions.cols) + && dimensions.cols > 0 && dimensions.cols !== terminal.cols) { terminal.resize(dimensions.cols, terminal.rows); } }; diff --git a/docs/examples/howto/unknown_length.py b/docs/examples/howto/unknown_length.py index c1b9a50c..99f63aed 100644 --- a/docs/examples/howto/unknown_length.py +++ b/docs/examples/howto/unknown_length.py @@ -8,6 +8,7 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 24 @@ -15,7 +16,10 @@ def main() -> None: widgets = [ 'Scanning: ', - progressbar.AnimatedMarker(), + progressbar.AnimatedMarker( + marker_wrap=colors.cyan1.fg('{}'), + default=colors.cyan1.fg('|'), + ), ' ', progressbar.Counter(), ' files found', diff --git a/docs/examples/readme/unknown_length.py b/docs/examples/readme/unknown_length.py index 1f25e101..88c77cd2 100644 --- a/docs/examples/readme/unknown_length.py +++ b/docs/examples/readme/unknown_length.py @@ -3,11 +3,13 @@ import time import progressbar +from progressbar.terminal import colors def main() -> None: with progressbar.ProgressBar( max_value=progressbar.UnknownLength, + widget_kwargs={'marker_wrap': colors.cyan1.fg('{}')}, ) as bar: for value in range(0, 120, 10): bar.update(value) diff --git a/docs/examples/tutorial/step2.py b/docs/examples/tutorial/step2.py index 227c3e47..ef01ba1e 100644 --- a/docs/examples/tutorial/step2.py +++ b/docs/examples/tutorial/step2.py @@ -9,10 +9,13 @@ import time import progressbar +from progressbar.terminal import colors def main() -> None: - with progressbar.ProgressBar() as bar: + with progressbar.ProgressBar( + widget_kwargs={'marker_wrap': colors.cyan1.fg('{}')}, + ) as bar: for i in range(100): time.sleep(0.01) bar.update(i + 1) diff --git a/docs/examples/widgets/animated_marker.py b/docs/examples/widgets/animated_marker.py index 0a8aa148..e539fab8 100644 --- a/docs/examples/widgets/animated_marker.py +++ b/docs/examples/widgets/animated_marker.py @@ -9,12 +9,19 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 24 def main() -> None: - widgets = ['Working: ', progressbar.AnimatedMarker()] + widgets = [ + 'Working: ', + progressbar.AnimatedMarker( + marker_wrap=colors.cyan1.fg('{}'), + default=colors.cyan1.fg('|'), + ), + ] with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: for step in range(STEPS): bar.update(step + 1) diff --git a/docs/examples/widgets/bouncing_bar.py b/docs/examples/widgets/bouncing_bar.py index 15fbf1e5..89f6bda9 100644 --- a/docs/examples/widgets/bouncing_bar.py +++ b/docs/examples/widgets/bouncing_bar.py @@ -12,12 +12,16 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 40 def main() -> None: - widgets = ['Working: ', progressbar.BouncingBar()] + widgets = [ + 'Working: ', + progressbar.BouncingBar(marker_wrap=colors.cyan1.fg('{}')), + ] with progressbar.ProgressBar( max_value=progressbar.UnknownLength, widgets=widgets, diff --git a/docs/examples/widgets/counter.py b/docs/examples/widgets/counter.py index 1d36e703..ce14191f 100644 --- a/docs/examples/widgets/counter.py +++ b/docs/examples/widgets/counter.py @@ -9,12 +9,17 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 24 def main() -> None: - widgets = ['Processed: ', progressbar.Counter(), ' lines'] + widgets = [ + 'Processed: ', + progressbar.Counter(format=colors.cyan1.fg('%(value)d')), + ' lines', + ] with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: for step in range(STEPS): bar.update(step + 1) diff --git a/docs/examples/widgets/format_label.py b/docs/examples/widgets/format_label.py index 9170e4de..cde752a4 100644 --- a/docs/examples/widgets/format_label.py +++ b/docs/examples/widgets/format_label.py @@ -8,6 +8,7 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 24 @@ -15,7 +16,7 @@ def main() -> None: widgets = [ progressbar.FormatLabel( - 'Processed: %(value)d lines (in: %(elapsed)s)' + colors.cyan1.fg('Processed: %(value)d lines (in: %(elapsed)s)') ), ] with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: diff --git a/docs/examples/widgets/timer.py b/docs/examples/widgets/timer.py index 9ef376a3..0062f8ba 100644 --- a/docs/examples/widgets/timer.py +++ b/docs/examples/widgets/timer.py @@ -14,12 +14,15 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 24 def main() -> None: - widgets = [progressbar.Timer()] + widgets = [ + progressbar.Timer(format=colors.cyan1.fg('Elapsed Time: %(elapsed)s')), + ] with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: for step in range(STEPS): bar.update(step + 1) diff --git a/docs/examples/widgets/unit_progress.py b/docs/examples/widgets/unit_progress.py index 591fbba2..24f83eed 100644 --- a/docs/examples/widgets/unit_progress.py +++ b/docs/examples/widgets/unit_progress.py @@ -9,12 +9,13 @@ import time import progressbar +from progressbar.terminal import colors STEPS = 24 def main() -> None: - widgets = [progressbar.UnitProgress(unit='files')] + widgets = [progressbar.UnitProgress(unit=colors.cyan1.fg('files'))] with progressbar.ProgressBar(max_value=STEPS, widgets=widgets) as bar: for step in range(STEPS): bar.update(step + 1) diff --git a/docs/howto/unknown-length.rst b/docs/howto/unknown-length.rst index db2b6cff..c47c7c3a 100644 --- a/docs/howto/unknown-length.rst +++ b/docs/howto/unknown-length.rst @@ -8,6 +8,9 @@ such as scanning a filesystem or reading a stream. .. demo:: howto/unknown-length +Each spinner frame is coloured cyan. Its colour stays fixed because +there is no percentage to drive a gradient. + Pass ``max_value=progressbar.UnknownLength`` and include an ``AnimatedMarker`` (or another marker-style widget) so there is still something visibly moving even without a percentage to report. A diff --git a/docs/tutorial/step2.rst b/docs/tutorial/step2.rst index c98fb22f..1fabfa33 100644 --- a/docs/tutorial/step2.rst +++ b/docs/tutorial/step2.rst @@ -10,7 +10,7 @@ the wrapper from the previous step and drives a ``ProgressBar`` by hand. The previous step gave ``range(100)`` to ``progressbar.progressbar()`` and let it manage everything. Here, the loop opens the bar as a context -manager with ``with progressbar.ProgressBar() as bar:`` and, on each pass +manager with ``with progressbar.ProgressBar(...) as bar:`` and, on each pass through its own ``for`` loop, calls ``bar.update(i + 1)`` to report the new value itself. The ``with`` block starts the bar on entry and finishes it on exit, just as ``progressbar.progressbar()`` did implicitly in step @@ -18,4 +18,7 @@ it on exit, just as ``progressbar.progressbar()`` did implicitly in step so it works just as well when progress doesn't come from iterating a sequence at all. +``marker_wrap`` colours the moving markers cyan. This bar has no known +total, so a fixed colour stays visible without a percentage gradient. + Next: :doc:`step3`. diff --git a/docs/widgets/animated-marker.rst b/docs/widgets/animated-marker.rst index b914ea95..585a6d67 100644 --- a/docs/widgets/animated-marker.rst +++ b/docs/widgets/animated-marker.rst @@ -19,6 +19,9 @@ Example .. demo:: widgets/animated-marker +``marker_wrap`` colours each moving frame cyan. ``default`` supplies +a coloured marker for the finished state. + See also -------------------------------------------------------------------------------- diff --git a/docs/widgets/bouncing-bar.rst b/docs/widgets/bouncing-bar.rst index f017ee6c..fe51ab5a 100644 --- a/docs/widgets/bouncing-bar.rst +++ b/docs/widgets/bouncing-bar.rst @@ -20,6 +20,8 @@ Example .. demo:: widgets/bouncing-bar +``marker_wrap`` colours the bouncing marker cyan, including its final position. + See also -------------------------------------------------------------------------------- diff --git a/docs/widgets/counter.rst b/docs/widgets/counter.rst index 9c0cf1b7..b8236b7d 100644 --- a/docs/widgets/counter.rst +++ b/docs/widgets/counter.rst @@ -18,6 +18,8 @@ Example .. demo:: widgets/counter +The format string colours the changing count cyan and leaves the label plain. + See also -------------------------------------------------------------------------------- diff --git a/docs/widgets/format-label.rst b/docs/widgets/format-label.rst index 544b0230..249dcf33 100644 --- a/docs/widgets/format-label.rst +++ b/docs/widgets/format-label.rst @@ -18,6 +18,8 @@ Example .. demo:: widgets/format-label +Colouring the format string makes the whole live label cyan. + See also -------------------------------------------------------------------------------- diff --git a/docs/widgets/timer.rst b/docs/widgets/timer.rst index 83dbc3ae..bee98dd8 100644 --- a/docs/widgets/timer.rst +++ b/docs/widgets/timer.rst @@ -20,6 +20,8 @@ Example .. demo:: widgets/timer +The format string keeps the elapsed time cyan throughout the run. + See also -------------------------------------------------------------------------------- diff --git a/docs/widgets/unit-progress.rst b/docs/widgets/unit-progress.rst index e94e3b2a..8853bb9d 100644 --- a/docs/widgets/unit-progress.rst +++ b/docs/widgets/unit-progress.rst @@ -18,6 +18,8 @@ Example .. demo:: widgets/unit-progress +The unit label is cyan for both the current count and the total. + See also -------------------------------------------------------------------------------- diff --git a/tests/console/test_console.py b/tests/console/test_console.py index d2cf26ca..03006da3 100644 --- a/tests/console/test_console.py +++ b/tests/console/test_console.py @@ -106,6 +106,7 @@ window.__consoleTestEvents = []; window.__consoleTestWorkerCount = 0; window.__consoleTestTerminal = null; +window.__consoleTestHasColour = false; (() => { const OriginalWorker = window.Worker; window.Worker = new Proxy(OriginalWorker, { @@ -131,6 +132,19 @@ construct(target, args) { const instance = new target(...args); window.__consoleTestTerminal = instance; + instance.onWriteParsed(() => { + const buffer = instance.buffer.active; + for (let y = 0; y < buffer.length; y++) { + const line = buffer.getLine(y); + for (let x = 0; x < instance.cols; x++) { + const cell = line.getCell(x); + if (cell.getChars().trim() && !cell.isFgDefault()) { + window.__consoleTestHasColour = true; + return; + } + } + } + }); return instance; }, }); diff --git a/tests/console/test_example_colours.py b/tests/console/test_example_colours.py new file mode 100644 index 00000000..8deb5f91 --- /dev/null +++ b/tests/console/test_example_colours.py @@ -0,0 +1,97 @@ +"""Run every published example and check its actual terminal colours.""" + +from __future__ import annotations + +import os +import pathlib +import re +import typing + +import pytest + +from .test_console import ( + BOOT_TIMEOUT_MS, + CONSOLE_TEST_INIT_SCRIPT, + ROOT, + browser as browser, + playwright_api, + server as server, +) + +if typing.TYPE_CHECKING: + from playwright.sync_api import Browser, BrowserContext, Locator, Page + + +def example_pages() -> list[tuple[str, str]]: + """Find every demo placed on a documentation page.""" + examples: list[tuple[str, str]] = [] + path: pathlib.Path + for path in sorted((ROOT / 'docs').rglob('*.rst')): + relative: pathlib.Path = path.relative_to(ROOT / 'docs') + if any(part.startswith('_') for part in relative.parts): + continue + names: list[str] = re.findall( + r'^\s*\.\. demo:: (\S+)', + path.read_text(encoding='utf-8'), + re.M, + ) + examples.extend( + (relative.with_suffix('.html').as_posix(), name) for name in names + ) + return sorted(set(examples)) + + +@pytest.fixture(scope='module') +def colour_context(browser: Browser) -> typing.Iterator[BrowserContext]: + context: BrowserContext = browser.new_context( + viewport={'width': 1440, 'height': 1000} + ) + context.add_init_script(CONSOLE_TEST_INIT_SCRIPT) + yield context + context.close() + + +@pytest.mark.parametrize('path,name', example_pages()) +def test_every_runnable_example_has_colour( + server: str, + colour_context: BrowserContext, + path: str, + name: str, +) -> None: + base_url: str = os.environ.get('DOCS_CONSOLE_BASE_URL', server) + page: Page = colour_context.new_page() + errors: list[str] = [] + page.on('pageerror', lambda error: errors.append(str(error))) + page.on( + 'console', + lambda message: ( + errors.append(message.text) + if message.type == 'error' + and not message.location.get('url', '').startswith( + 'https://media.ethicalads.io/' + ) + else None + ), + ) + try: + page.goto(f'{base_url.rstrip("/")}/{path}') + panel: Locator = page.locator(f'.demo-run[data-demo="{name}"]') + if name in {'howto/multibar', 'howto/parallel-execution'}: + playwright_api.expect(panel).to_have_class( + re.compile('demo-run-unavailable') + ) + return + button: Locator = panel.get_by_role('button', name='Run', exact=True) + button.click() + page.wait_for_function( + 'window.__consoleTestEvents.some(event => ' + "event.type === 'done' || event.type === 'error')", + timeout=BOOT_TIMEOUT_MS, + ) + assert not page.evaluate( + "window.__consoleTestEvents.some(event => event.type === 'error')" + ), panel.inner_text() + page.wait_for_function('window.__consoleTestHasColour', timeout=3000) + assert not errors + finally: + page.close() diff --git a/tests/test_readme_demos.py b/tests/test_readme_demos.py index 910ed1b3..0f53b2d4 100644 --- a/tests/test_readme_demos.py +++ b/tests/test_readme_demos.py @@ -1019,7 +1019,11 @@ def test_bouncing_bar_demo_respects_narrow_term_width() -> None: assert demo.term_width == 30 frames = demos.capture_demo(demo) - widths = [len(line) for frame in frames for line in frame] + widths: list[int] = [ + len(demos.ANSI_SGR_RE.sub('', line)) + for frame in frames + for line in frame + ] assert widths assert max(widths) <= demo.term_width From 83c003f8842a0e47d7e8adbf15251cc6e39d4d41 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Mon, 14 Sep 2026 12:57:21 +0200 Subject: [PATCH 17/18] Colour widgets by default and cycle spinner colours --- README.md | 17 +-- docs/_ext/demo.py | 6 +- docs/_static/demos/howto-colors.svg | 4 +- docs/_static/demos/howto-iterable-wrapper.svg | 2 +- .../demos/howto-logging-integration.svg | 2 +- .../demos/howto-multibar-line-offset.svg | 2 +- docs/_static/demos/howto-multibar.svg | 2 +- docs/_static/demos/howto-non-tty.svg | 2 +- .../demos/howto-parallel-execution.svg | 2 +- docs/_static/demos/howto-prefix-suffix.svg | 2 +- docs/_static/demos/howto-redirect-stdout.svg | 2 +- docs/_static/demos/howto-tqdm-style.svg | 2 +- docs/_static/demos/howto-unknown-length.svg | 2 +- docs/_static/demos/readme-cli.svg | 2 +- docs/_static/demos/readme-colors.svg | 2 +- docs/_static/demos/readme-hero.svg | 2 +- docs/_static/demos/readme-multibar.svg | 2 +- docs/_static/demos/readme-parallel.svg | 2 +- docs/_static/demos/readme-unknown-length.svg | 2 +- docs/_static/demos/tutorial-step1.svg | 2 +- docs/_static/demos/tutorial-step2.svg | 2 +- docs/_static/demos/tutorial-step3.svg | 2 +- docs/_static/demos/tutorial-step4.svg | 2 +- docs/_static/demos/tutorial-step5.svg | 2 +- docs/_static/demos/widgets-absolute-eta.svg | 2 +- docs/_static/demos/widgets-adaptive-eta.svg | 2 +- .../_static/demos/widgets-animated-marker.svg | 2 +- docs/_static/demos/widgets-bouncing-bar.svg | 2 +- docs/_static/demos/widgets-counter.svg | 2 +- docs/_static/demos/widgets-eta.svg | 2 +- docs/_static/demos/widgets-format-label.svg | 2 +- docs/_static/demos/widgets-granular-bar.svg | 2 +- .../_static/demos/widgets-rotating-marker.svg | 2 +- docs/_static/demos/widgets-smoothing-eta.svg | 2 +- docs/_static/demos/widgets-timer.svg | 2 +- docs/_static/demos/widgets-unit-progress.svg | 2 +- docs/_static/livecode/livecode.js | 2 +- docs/_static/livecode/worker.js | 2 +- docs/examples/howto/colors.py | 17 ++- docs/examples/howto/unknown_length.py | 6 +- docs/examples/readme/colors.py | 15 +-- docs/examples/readme/unknown_length.py | 2 - docs/examples/tutorial/step2.py | 5 +- docs/examples/widgets/animated_marker.py | 9 +- docs/examples/widgets/bouncing_bar.py | 6 +- docs/examples/widgets/counter.py | 7 +- docs/examples/widgets/format_label.py | 3 +- docs/examples/widgets/timer.py | 5 +- docs/examples/widgets/unit_progress.py | 3 +- docs/howto/colors.rst | 25 ++-- docs/howto/unknown-length.rst | 3 - docs/tutorial/step2.rst | 8 +- docs/widgets/animated-marker.rst | 6 +- docs/widgets/bouncing-bar.rst | 2 - docs/widgets/counter.rst | 2 - docs/widgets/format-label.rst | 2 - docs/widgets/timer.rst | 2 - docs/widgets/unit-progress.rst | 2 - progressbar/widgets.py | 94 +++++++++------ scripts/build_docs_wheels.py | 13 ++- tests/console/test_console.py | 10 +- tests/console/test_example_colours.py | 8 ++ tests/test_color.py | 6 +- tests/test_docs_wheel_urls.py | 27 +++++ tests/test_multibar.py | 2 + tests/test_widget_default_colours.py | 109 ++++++++++++++++++ tests/test_widgets.py | 5 +- 67 files changed, 323 insertions(+), 178 deletions(-) create mode 100644 tests/test_docs_wheel_urls.py create mode 100644 tests/test_widget_default_colours.py diff --git a/README.md b/README.md index 373d7eb8..65dc19cc 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,9 @@ terminal through one `MultiBar`: `gradient_colors` shifts a bar's fill color as its percentage grows, so the download bar sweeps red through gold to green and the render bar -sweeps sky blue into fuchsia. The scan bar has no percentage to sweep -(its length is unknown), so `fixed_colors` gives its animated marker one -unchanging cyan instead. +sweeps sky blue into fuchsia. The scan spinner normally cycles through +colours with its frames. Passing a single cyan colour keeps its colour +unchanged while the marker spins. """ import sys @@ -89,7 +89,7 @@ import time import progressbar from progressbar.terminal import ColorGradient, colors -from progressbar.widgets import TFixedColors, TGradientColors +from progressbar.widgets import TGradientColors STEPS = 24 @@ -122,11 +122,8 @@ def main() -> None: multibar['scan'] = progressbar.ProgressBar( max_value=progressbar.UnknownLength, widgets=[ - progressbar.Bar( - marker=progressbar.AnimatedMarker(), - fixed_colors=TFixedColors( - fg_none=colors.cyan1, bg_none=None - ), + progressbar.AnimatedMarker( + gradient_colors=TGradientColors(fg=colors.cyan1, bg=None), ), ], ) @@ -350,13 +347,11 @@ with a counter instead of a percentage: import time import progressbar -from progressbar.terminal import colors def main() -> None: with progressbar.ProgressBar( max_value=progressbar.UnknownLength, - widget_kwargs={'marker_wrap': colors.cyan1.fg('{}')}, ) as bar: for value in range(0, 120, 10): bar.update(value) diff --git a/docs/_ext/demo.py b/docs/_ext/demo.py index a2689d33..8451d58d 100644 --- a/docs/_ext/demo.py +++ b/docs/_ext/demo.py @@ -178,5 +178,9 @@ def setup(app: Sphinx) -> dict[str, typing.Any]: app.add_css_file('livecode/livecode.css') app.add_js_file('vendor/xterm.js') app.add_js_file('vendor/addon-fit.js') - app.add_js_file('livecode/livecode.js') + worker_path: pathlib.Path = REPO_ROOT / 'docs/_static/livecode/worker.js' + worker_hash: str = hashlib.sha256(worker_path.read_bytes()).hexdigest() + app.add_js_file( + 'livecode/livecode.js', **{'data-worker-version': worker_hash} + ) return {'parallel_read_safe': True, 'parallel_write_safe': True} diff --git a/docs/_static/demos/howto-colors.svg b/docs/_static/demos/howto-colors.svg index 71593662..48c901d4 100644 --- a/docs/_static/demos/howto-colors.svg +++ b/docs/_static/demos/howto-colors.svg @@ -7,7 +7,7 @@ viewBox="0 0 1080 96" > Fixed and gradient bar colors - Color a bar with a fixed color or a gradient that shifts with progress. + Choose a progress gradient and a solid colour for an animated spinner.