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