Skip to content

Commit d3793ea

Browse files
committed
refactor(bar): cooperative update/start/finish dispatch
Replace explicit class-qualified parent calls in the update/start/finish chains with cooperative super() dispatch, mirroring the __init__ migration. - ProgressBar._update_parents: three explicit parent update() calls (two of which resolved to the no-op ProgressBarMixinBase.update) collapse to a single super().update(value=value). - ProgressBar.start / .finish: three explicit parent calls each collapse to one super() call. - StdRedirectMixin/DefaultFdMixin/ResizableMixin internal explicit parent calls converted to super(). value is passed by keyword through the chain so the intermediate `*args, **kwargs` and `value=None` signatures interoperate. Ordering note: the SIGWINCH uninstall in ResizableMixin.finish now runs before the stream unwrap in StdRedirectMixin.finish (previously after). The two subsystems are independent, so behavior is unchanged. Add two characterization tests: a super()-style update override is entered exactly once per update() call (guards the collapsed chain against re-dispatch), and finish(end='') threads end through the collapsed chain.
1 parent d27d5c5 commit d3793ea

2 files changed

Lines changed: 91 additions & 15 deletions

File tree

progressbar/bar.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def start(self, **kwargs: typing.Any):
339339
super().start()
340340

341341
def update(self, *args: types.Any, **kwargs: types.Any) -> None:
342-
ProgressBarMixinBase.update(self, *args, **kwargs)
342+
super().update(*args, **kwargs)
343343

344344
line: str = converters.to_unicode(self._format_line())
345345
if not self.enable_colors:
@@ -363,7 +363,7 @@ def finish(
363363
return
364364

365365
end = kwargs.pop('end', '\n')
366-
ProgressBarMixinBase.finish(self, *args, **kwargs)
366+
super().finish(*args, **kwargs)
367367

368368
if end and not self.line_breaks:
369369
self.fd.write(end)
@@ -500,7 +500,7 @@ def _handle_resize(
500500
self.term_width = w
501501

502502
def finish(self): # pragma: no cover
503-
ProgressBarMixinBase.finish(self)
503+
super().finish()
504504
if self.signal_set:
505505
with contextlib.suppress(Exception):
506506
_ResizeRegistry.uninstall(self)
@@ -556,7 +556,7 @@ def start(self, *args: typing.Any, **kwargs: typing.Any):
556556
self.stderr = utils.streams.stderr
557557

558558
utils.streams.start_capturing(self)
559-
DefaultFdMixin.start(self, *args, **kwargs)
559+
super().start(*args, **kwargs)
560560

561561
def update(self, value: types.Optional[NumberT] = None):
562562
cleared = not self.line_breaks and utils.streams.needs_clear()
@@ -567,10 +567,10 @@ def update(self, value: types.Optional[NumberT] = None):
567567
if cleared and self.redirect_blank_line:
568568
# Keep a blank line between the redirected output and the bar
569569
self.fd.write('\n')
570-
DefaultFdMixin.update(self, value=value)
570+
super().update(value=value)
571571

572572
def finish(self, end='\n'):
573-
DefaultFdMixin.finish(self, end=end)
573+
super().finish(end=end)
574574
utils.streams.stop_capturing(self)
575575
if self.redirect_stdout:
576576
utils.streams.unwrap_stdout()
@@ -1250,9 +1250,11 @@ def _update_variables(self, kwargs):
12501250

12511251
def _update_parents(self, value: ValueT):
12521252
self.updates += 1
1253-
ResizableMixin.update(self, value=value)
1254-
ProgressBarBase.update(self, value=value)
1255-
StdRedirectMixin.update(self, value=value) # type: ignore
1253+
# Cooperative dispatch through the MRO
1254+
# (StdRedirectMixin -> DefaultFdMixin -> ProgressBarMixinBase). The
1255+
# `value` is passed by keyword so the intermediate `*args, **kwargs`
1256+
# and `value=None` signatures interoperate.
1257+
super().update(value=value) # type: ignore
12561258

12571259
# Only flush if something was actually written
12581260
self.fd.flush()
@@ -1293,9 +1295,10 @@ def start(
12931295
if self.max_value is None:
12941296
self.max_value = self._DEFAULT_MAXVAL
12951297

1296-
StdRedirectMixin.start(self, max_value=max_value)
1297-
ResizableMixin.start(self, max_value=max_value)
1298-
ProgressBarBase.start(self, max_value=max_value)
1298+
# Cooperative dispatch through the MRO
1299+
# (StdRedirectMixin -> DefaultFdMixin -> ProgressBarMixinBase);
1300+
# ResizableMixin/ProgressBarBase define no `start` and are skipped.
1301+
super().start(max_value=max_value)
12991302

13001303
# Constructing the default widgets is only done when we know max_value
13011304
if not self.widgets:
@@ -1385,9 +1388,13 @@ def finish(self, end: str = '\n', dirty: bool = False):
13851388
self.end_time = datetime.now()
13861389
self.update(self.max_value, force=True)
13871390

1388-
StdRedirectMixin.finish(self, end=end)
1389-
ResizableMixin.finish(self)
1390-
ProgressBarBase.finish(self)
1391+
# Cooperative dispatch through the MRO
1392+
# (StdRedirectMixin -> DefaultFdMixin -> ResizableMixin ->
1393+
# ProgressBarMixinBase). Ordering note: the SIGWINCH uninstall in
1394+
# ResizableMixin.finish now runs *before* the stream unwrap in
1395+
# StdRedirectMixin.finish (previously it ran after). The two
1396+
# subsystems are independent, so the observable result is unchanged.
1397+
super().finish(end=end)
13911398

13921399
@property
13931400
def currval(self):

tests/test_subclass_compat.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,72 @@ def test_old_style_triple_call_bar_consumes_one_index() -> None:
423423
# parent __init__ entry points reaching ProgressBarBase.
424424
assert first.index >= 0
425425
assert second.index == first.index + 1
426+
427+
428+
# --- update/start/finish chain: cooperative-super() guarantees --------------
429+
430+
431+
def test_super_style_update_override_dispatched_once() -> None:
432+
"""A super()-style ``update`` override runs exactly once per call.
433+
434+
The collapsed ``_update_parents`` chain dispatches to the *parent*
435+
mixins via ``super().update(...)``, so it must never re-enter the
436+
subclass's own ``update`` override. A double dispatch through the
437+
chain would bump the counter twice.
438+
"""
439+
calls = 0
440+
441+
class CountingBar(progressbar.ProgressBar):
442+
def update(
443+
self,
444+
value: typing.Any = None,
445+
force: bool = False,
446+
**kwargs: typing.Any,
447+
) -> None:
448+
nonlocal calls
449+
calls += 1
450+
super().update(value, force=force, **kwargs)
451+
452+
bar = CountingBar(fd=io.StringIO(), max_value=10, term_width=60)
453+
# start() itself calls update(min_value); ignore those bootstrap calls.
454+
bar.start()
455+
calls = 0
456+
bar.update(1, force=True)
457+
assert calls == 1
458+
bar.finish()
459+
460+
461+
def test_finish_end_kwarg_threads_through_chain() -> None:
462+
"""``finish(end='')`` still threads ``end`` through the collapsed chain.
463+
464+
``end`` is popped inside ``DefaultFdMixin.finish`` after the migration;
465+
an empty value must suppress the trailing newline while the default
466+
still writes one.
467+
"""
468+
fd_blank = io.StringIO()
469+
bar = progressbar.ProgressBar(
470+
fd=fd_blank,
471+
max_value=10,
472+
term_width=60,
473+
enable_colors=False,
474+
line_breaks=False,
475+
)
476+
bar.start()
477+
bar.update(5, force=True)
478+
bar.finish(end='')
479+
assert bar.finished()
480+
assert not fd_blank.getvalue().endswith('\n')
481+
482+
# Contrast: the default end='\n' still writes the trailing newline.
483+
fd_newline = io.StringIO()
484+
bar2 = progressbar.ProgressBar(
485+
fd=fd_newline,
486+
max_value=10,
487+
term_width=60,
488+
enable_colors=False,
489+
line_breaks=False,
490+
)
491+
bar2.start()
492+
bar2.update(5, force=True)
493+
bar2.finish()
494+
assert fd_newline.getvalue().endswith('\n')

0 commit comments

Comments
 (0)