From 837be153e13e70e23ea5bb340d750b659732cc52 Mon Sep 17 00:00:00 2001 From: Rick van Hattem Date: Wed, 9 Sep 2026 01:12:25 +0200 Subject: [PATCH] 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