Skip to content

Quality audit PR 3/6: bar.py internals, decompositions, start() race fix#321

Merged
wolph merged 15 commits into
developfrom
quality-audit-3-internals
Jul 6, 2026
Merged

Quality audit PR 3/6: bar.py internals, decompositions, start() race fix#321
wolph merged 15 commits into
developfrom
quality-audit-3-internals

Conversation

@wolph

@wolph wolph commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Third of six audit PRs (PR 1: #319, PR 2: #320). Behavior-preserving internal restructuring plus one real concurrency bug fix.

Bug fix

  • start() ordering race (d7cb0c1): _started flipped true before self.widgets was populated, so a concurrent reader — MultiBar's render thread calls bar.started() then asserts bar.widgets — could crash on the empty-widgets window (this exact crash appeared as a reproducible py312 CI coverage failure during PR 1). Widget/state setup now completes before _started becomes observable; deterministic regression test captures the widget list at the exact flip.

Decompositions (no behavior change; suite + render goldens + byte-identical CLI output as oracle)

  • ProgressBar.__init__ (~100 lines) → four single-purpose private helpers; the self-deprecating # sourcery skip: low-code-quality is gone (842b6d4).
  • __main__.main() (125 lines, noqa: C901) → _resolve_inputs / _build_widgets / _transfer; passes the complexity check naturally now (ca6553f).
  • ColorSupport.from_env → per-source helpers with the subtle precedence preserved (3aaf461).
  • ETA family: shared _resolve_value_elapsed; the UnknownLength path is now an explicit check instead of a caught TypeError (guarded to keep elapsed == 0 rendering format_zero, byte-identical) (59b3cb2).
  • Bar family: _render_borders helper replaces the border preamble copy-pasted across 4 subclasses (GranularBar keeps its copy — different MRO branch — with a pointer comment) (5927aae).

Internals

  • data() is now a pure snapshot; the timer reset moved to _mark_update() on the draw path (gate calibration cadence verified unchanged by the fastpath parity tests) (04d57e8).
  • _load_widgets() cached via functools.cache — full-bar render sites no longer pay an import_module lookup per call; the fast path still never imports widgets (test-enforced) (8c82288).
  • _format_fast_line documented as the optional native-formatter extension point (007f5f1).
  • API snapshot test made order-independent under freezegun (960657e).

Verification

569 passed / 100.00% branch coverage (3.14); CI-parity (TERM=dumb) green on 3.10/3.12; ruff + pyright clean; perf budget passes; multibar suite 3× consecutive green; CLI output byte-compared identical.

wolph added 10 commits July 6, 2026 04:06
ProgressBar.start() called super().start() (which sets _started=True via
ProgressBarMixinBase.start) before building default_widgets(), so a
concurrent reader — e.g. MultiBar's render thread reading bar.started()
then asserting bar.widgets in _label_bar — could observe started() True
with an empty widget list and crash.

Move the cooperative super().start() dispatch to run after all
widget/prefix/suffix/poll/gate/max_value setup completes. The 0% draw
still happens at the same point (stream redirect and console mode are set
up in super().start(), before the draw). Add a deterministic regression
test capturing the widget list at the exact _started flip.
data() secretly reset _last_update_time/_last_update_timer on every call,
so a getter mutated timing state. Extract the reset into a private
_mark_update() and call it from _update_parents() (the redraw path), where
the gate calibration in _draw_and_recalibrate already expects the timer to
be refreshed during the draw. data() is now a pure read with no timing
side effects; the public last_update_time and gate cadence are unchanged
because the stamp still happens once per redraw at the same point.

Add a regression test (deterministic advancing clock) asserting two
consecutive data() calls leave the timing fields untouched.
_load_widgets() called importlib.import_module on every invocation — once
per full-bar render site (_format_widgets, default_widgets, prefix/suffix
init). The module is immutable after first import, so wrap the helper in
functools.cache: importlib resolves progressbar.widgets once on first use
and reuses the object thereafter. Name/signature unchanged; the fast path
still never calls _load_widgets() so it stays widgets-free (lazy-import
tests remain green).
The '_format_fast_line ... Wired in a later task' comment described work
that never happened and read as dead indirection. It is in fact a tested,
supported extension point (test_fast_format_line_uses_native_hook
monkeypatches it, and _format_line prefers it over the pure-Python
formatter). Keep the hook and the fallback; replace the misleading comment
with an accurate description of the optional native/custom formatter hook.
Also annotate default_widgets(self) -> list[typing.Any].
The constructor mixed alias deprecation, widget copying, poll-interval
setup, and variable seeding — enough to warrant a
'sourcery skip: low-code-quality' marker. Extract four private,
single-purpose helpers and reduce __init__ to orchestration:

- _apply_deprecated_aliases(): resolve maxval/poll DeprecationWarnings,
  returns the updated (max_value, poll_interval)
- _copy_widgets(): deepcopy the copy-safe widgets into a fresh list
- _setup_poll_intervals(): timedelta->seconds conversion and clamping
- _seed_variables(): variables dict + VariableMixin widget-name scan

Order of operations is unchanged, so construction behavior is identical.
__init__'s signature is byte-identical (only the sourcery comment is
dropped, no longer needed).
If an earlier test imports a module while freezegun is active, module
constants like widgets.MAX_DATE are Fake* instances for the rest of the
process; describe instances by their first non-freezegun MRO class so
the snapshot no longer depends on test collection order.
Extract the input resolution, widget selection and copy loop out of the
125-line main() god function into three private single-purpose helpers:

- _resolve_inputs(args, parser): stdin/file resolution + total-size detection
- _build_widgets(args, filesize_available): widget-set selection
- _transfer(bar, input_paths, output_stream, args, stack): the copy loop

main() is now pure orchestration and passes flake8 C901 without the
'# noqa: C901' suppression, which is removed. Behavior-preserving: CLI
output is byte-identical and the pty-based command tests pass unchanged.
from_env() mixed three unrelated detection strategies inline. Extract each
into a focused classmethod, leaving from_env() as a precedence-ordered
dispatcher:

- _from_jupyter(): interactive-kernel true-color shortcut
- _from_windows(): Windows console-mode probe
- _from_term_variables(variables): terminal env-var depth scan

Behavior-preserving: the JUPYTER -> Windows -> env-scan precedence and the
subtle scan semantics (first truecolor/24bit wins via break; otherwise the
highest depth wins via max(); a generic truthy FORCE_COLOR=1 returns full
color) are unchanged. The 'elif os.name == "nt"' dispatch line keeps the
existing coverage exclusion; _from_windows() carries the '# pragma: no cover'
that previously sat on the Windows block (unreachable on non-Windows CI).
ETA.__call__ and SmoothingETA.__call__ shared an identical value/elapsed
default-resolution preamble. Extract it into ETA._resolve_value_elapsed()
and call it from both; the shared helper's branches are covered by the ETA
paths so SmoothingETA no longer needs its two '# pragma: no branch'.

Also replace the TypeError-as-control-flow around _calculate_eta with an
explicit 'progress.max_value is base.UnknownLength' check. The only source
of that TypeError was subtracting the value from the UnknownLength sentinel
inside _calculate_eta's 'if elapsed:' branch, so the explicit check is
guarded by 'elapsed' too: this keeps the elapsed==0 case returning 0
(format_zero) instead of N/A, preserving byte-identical output. Nothing
else in the ETA math raises TypeError, so the try/except is removed
entirely rather than kept as a defensive handler.

Behavior-preserving: full suite + render goldens unchanged, widgets.py at
100% branch coverage.
…preamble

The identical left/right border resolution (resolve callables, subtract
their visible length from width) was copy-pasted across five bar __call__
methods. Extract it into Bar._render_borders(progress, data, width) ->
(left, right, remaining_width) and call it from Bar, BouncingBar,
MultiRangeBar and JobStatusBar (all Bar subclasses).

GranularBar descends from AutoWidthWidgetBase (not Bar) so it can't reach
the helper; rather than hoisting border concerns onto a shared width-widget
base (a responsibility smell affecting widgets that have no borders) or
changing any MRO, its copy is kept with an explanatory comment.

Behavior-preserving: widget render output byte-identical, full suite +
render goldens pass, widgets.py at 100% branch coverage.
Copilot AI review requested due to automatic review settings July 6, 2026 02:46
Comment thread tests/test_progressbar.py Fixed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors several modules in the progressbar library to improve code quality, readability, and structure. Key changes include extracting helper methods in __main__.py, bar.py, env.py, and widgets.py, caching lazy widget imports, and separating timing side effects from the data() state snapshot. Additionally, it addresses a race condition in ProgressBar.start() where the started flag was set before widgets were populated. The review feedback highlights two potential TypeError bugs: one in widgets.py when progress.max_value is None for indeterminate bars, and another in test_api_surface.py if cls.__module__ is not a string.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread progressbar/widgets.py Outdated
Comment thread tests/test_api_surface.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues the quality audit series by refactoring internal progress bar code for clarity/perf, tightening a few behavioral contracts with regression tests, and addressing a concurrency race in ProgressBar.start() visibility.

Changes:

  • Fixes a start() ordering race so _started becomes observable only after widgets are populated (MultiBar render-thread safety).
  • Refactors internals for maintainability/performance (e.g., cached lazy widget import, decomposed init/start logic, shared helpers in ETA/bar widgets).
  • Adds/adjusts regression tests to lock in purity/order-independence guarantees (data() snapshot purity; API surface snapshot stability under freezegun).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_progressbar.py Adds a regression test ensuring data() is a pure snapshot (no timing side effects).
tests/test_multibar.py Adds a regression test guarding the _started/widgets ordering race relevant to MultiBar.
tests/test_api_surface.py Makes API surface snapshot stable even when earlier tests import modules under freezegun.
progressbar/widgets.py Refactors ETA value/elapsed resolution and deduplicates bar border rendering logic.
progressbar/fast.py Documents the native formatter hook and tightens typing on default_widgets.
progressbar/env.py Decomposes ColorSupport.from_env into precedence-preserving helpers.
progressbar/bar.py Caches _load_widgets, decomposes __init__, moves timing stamps out of data() into _mark_update, and reorders start() for race safety.
progressbar/main.py Refactors CLI main() into helpers for input resolution, widget selection, and transfer loop.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread progressbar/__main__.py Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5927aae5dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread progressbar/bar.py
wolph added 5 commits July 6, 2026 05:04
The _transfer extraction moved it outside, so BrokenPipeError from a
piped stderr could crash shutdown (flagged by review on #321); restore
the historical placement.
A MultiBar render thread that sees started() True calls
update(force=True), and update() re-enters start() while start_time is
None -- double-running the stream-capturing path. Populate
start_time/last_update_time before the cooperative start() dispatch
flips _started (flagged by review on #321); flip-observation test
extended to assert it.
The explicit UnknownLength check replaced a broad try/except TypeError;
None is also a legitimate indeterminate max_value and must take the N/A
path (flagged by review on #321).
@wolph wolph merged commit 3f4935c into develop Jul 6, 2026
23 checks passed
@wolph wolph deleted the quality-audit-3-internals branch July 6, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants