diff --git a/pyproject.toml b/pyproject.toml index 1f296839..b06efc77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,6 @@ select = [ ] extend-ignore = [ "A003", # builtin-attribute-shadowing - "B028", # no-explicit-stacklevel "C408", # unnecessary-collection-call "ISC003", # explicit-string-concatenation "PLC1901", # compare-to-empty-string diff --git a/src/streamlink/options.py b/src/streamlink/options.py index 00ef5d6d..58d2d446 100644 --- a/src/streamlink/options.py +++ b/src/streamlink/options.py @@ -136,6 +136,9 @@ class Argument: warnings.warn( "Defining global plugin arguments is deprecated. Use the session options instead.", StreamlinkDeprecationWarning, + # set stacklevel to 3 because of the @pluginargument decorator + # which is the public interface for defining plugin arguments + stacklevel=3, ) @staticmethod diff --git a/src/streamlink/plugin/api/validate/__init__.py b/src/streamlink/plugin/api/validate/__init__.py index 36898153..ead3fe3d 100644 --- a/src/streamlink/plugin/api/validate/__init__.py +++ b/src/streamlink/plugin/api/validate/__init__.py @@ -64,7 +64,7 @@ def _deprecations(): from streamlink.exceptions import StreamlinkDeprecationWarning val, msg = deprecations[_attr] - warnings.warn(msg, StreamlinkDeprecationWarning) + warnings.warn(msg, StreamlinkDeprecationWarning, stacklevel=2) return val diff --git a/src/streamlink/plugin/plugin.py b/src/streamlink/plugin/plugin.py index 0af787f2..3c54ed9d 100644 --- a/src/streamlink/plugin/plugin.py +++ b/src/streamlink/plugin/plugin.py @@ -306,6 +306,7 @@ class Plugin: warnings.warn( f"Initialized {self.module} plugin with deprecated constructor", FutureWarning, + stacklevel=2, ) # Wrapper class which comes after the deprecated plugin in the MRO diff --git a/src/streamlink/session.py b/src/streamlink/session.py index 9f2f6d23..bc55e8da 100644 --- a/src/streamlink/session.py +++ b/src/streamlink/session.py @@ -27,6 +27,21 @@ log = logging.getLogger(__name__) _original_allowed_gai_family = urllib3_util_connection.allowed_gai_family # type: ignore[attr-defined] +def _get_deprecation_stacklevel_offset(): + """Deal with stacklevels of both session.{g,s}et_option() and session.options.{g,s}et() calls""" + from inspect import currentframe + + frame = currentframe().f_back.f_back + offset = 0 + while frame: + if frame.f_code.co_filename == __file__ and frame.f_code.co_name in ("set_option", "get_option"): + offset += 1 + break + frame = frame.f_back + + return offset + + class PythonDeprecatedWarning(UserWarning): pass @@ -47,14 +62,19 @@ class StreamlinkOptions(Options): except ValueError: continue - # ---- getters - - def _get_http_proxy(self, key): + @staticmethod + def _deprecate_https_proxy(key: str) -> None: if key == "https-proxy": warnings.warn( "The `https-proxy` option has been deprecated in favor of a single `http-proxy` option", StreamlinkDeprecationWarning, + stacklevel=4 + _get_deprecation_stacklevel_offset(), ) + + # ---- getters + + def _get_http_proxy(self, key): + self._deprecate_https_proxy(key) return self.session.http.proxies.get("https" if key == "https-proxy" else "http") def _get_http_attr(self, key): @@ -88,11 +108,7 @@ class StreamlinkOptions(Options): self.session.http.proxies["http"] \ = self.session.http.proxies["https"] \ = update_scheme("https://", value, force=False) - if key == "https-proxy": - warnings.warn( - "The `https-proxy` option has been deprecated in favor of a single `http-proxy` option", - StreamlinkDeprecationWarning, - ) + self._deprecate_https_proxy(key) def _set_http_attr(self, key, value): setattr(self.session.http, self._OPTIONS_HTTP_ATTRS[key], value) @@ -124,6 +140,7 @@ class StreamlinkOptions(Options): warnings.warn( f"`{key}` has been deprecated in favor of the `{name}` option", StreamlinkDeprecationWarning, + stacklevel=3 + _get_deprecation_stacklevel_offset(), ) return inner @@ -547,6 +564,7 @@ class Streamlink: warnings.warn( f"Resolved plugin {name} with deprecated can_handle_url API", StreamlinkDeprecationWarning, + stacklevel=1, ) candidate = name, plugin priority = prio diff --git a/src/streamlink_cli/main.py b/src/streamlink_cli/main.py index 88f9bd57..f420f8d8 100644 --- a/src/streamlink_cli/main.py +++ b/src/streamlink_cli/main.py @@ -610,6 +610,7 @@ def load_plugins(dirs: List[Path], showwarning: bool = True): warnings.warn( f"Loaded plugins from deprecated path, see CLI docs for how to migrate: {directory}", StreamlinkDeprecationWarning, + stacklevel=1, ) elif showwarning: log.warning(f"Plugin path {directory} does not exist or is not a directory!") @@ -658,6 +659,7 @@ def setup_config_args(parser, ignore_unknown=False): warnings.warn( f"Loaded config from deprecated path, see CLI docs for how to migrate: {config_file}", StreamlinkDeprecationWarning, + stacklevel=1, ) config_files.append(config_file) break @@ -674,6 +676,7 @@ def setup_config_args(parser, ignore_unknown=False): warnings.warn( f"Loaded plugin config from deprecated path, see CLI docs for how to migrate: {config_file}", StreamlinkDeprecationWarning, + stacklevel=1, ) config_files.append(config_file) break diff --git a/tests/test_api_validate.py b/tests/test_api_validate.py index bd4521a3..38b9ca3b 100644 --- a/tests/test_api_validate.py +++ b/tests/test_api_validate.py @@ -19,10 +19,11 @@ def test_text_is_str(recwarn: pytest.WarningsRecorder): assert "text" not in getattr(validate, "__dict__", {}) assert "text" in getattr(validate, "__all__", []) assert validate.text is str, "Exports text as str alias for backwards compatiblity" - assert [(record.category, str(record.message)) for record in recwarn.list] == [ + assert [(record.category, str(record.message), record.filename) for record in recwarn.list] == [ ( StreamlinkDeprecationWarning, "`streamlink.plugin.api.validate.text` is deprecated. Use `str` instead.", + __file__, ), ] diff --git a/tests/test_options.py b/tests/test_options.py index 2c4bc736..8a943247 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -242,8 +242,12 @@ class TestSetupOptions: def _get_streams(self): # pragma: no cover pass - assert [(record.category, str(record.message)) for record in recwarn.list] == [ - (StreamlinkDeprecationWarning, "Defining global plugin arguments is deprecated. Use the session options instead."), + assert [(record.category, str(record.message), record.filename) for record in recwarn.list] == [ + ( + StreamlinkDeprecationWarning, + "Defining global plugin arguments is deprecated. Use the session options instead.", + __file__, + ), ] session = Mock() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 59521f95..628f7565 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -85,8 +85,12 @@ class TestPlugin: assert isinstance(plugin, DeprecatedPlugin) assert plugin.custom_attribute == "HTTP://LOCALHOST" - assert [(record.category, str(record.message)) for record in recwarn.list] == [ - (FutureWarning, "Initialized test_plugin plugin with deprecated constructor"), + assert [(record.category, str(record.message), record.filename) for record in recwarn.list] == [ + ( + FutureWarning, + "Initialized test_plugin plugin with deprecated constructor", + __file__, + ), ] assert plugin.session is session diff --git a/tests/test_session.py b/tests/test_session.py index 7fa1dfdd..116854d9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -10,9 +10,9 @@ import requests_mock import urllib3 import tests.plugin -from streamlink import NoPluginError, Streamlink -from streamlink.exceptions import StreamlinkDeprecationWarning +from streamlink.exceptions import NoPluginError, StreamlinkDeprecationWarning from streamlink.plugin import HIGH_PRIORITY, LOW_PRIORITY, NO_PRIORITY, NORMAL_PRIORITY, Plugin, pluginmatcher +from streamlink.session import Streamlink from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream @@ -418,10 +418,11 @@ class TestSessionOptionHttpProxy: @pytest.fixture() def _logs_deprecation(self, recwarn: pytest.WarningsRecorder): yield - assert [(record.category, str(record.message)) for record in recwarn.list] == [ + assert [(record.category, str(record.message), record.filename) for record in recwarn.list] == [ ( StreamlinkDeprecationWarning, "The `https-proxy` option has been deprecated in favor of a single `http-proxy` option", + __file__, ), ] @@ -481,6 +482,16 @@ class TestSessionOptionHttpProxy: session.http.proxies["https"] = "http://testproxy2.com" assert session.get_option("https-proxy") == "http://testproxy2.com" + @pytest.mark.usefixtures("_logs_deprecation") + def test_https_proxy_get_directly(self, session: Streamlink): + # The DeprecationWarning's origin must point to this call, even without the set_option() wrapper + session.options.get("https-proxy") + + @pytest.mark.usefixtures("_logs_deprecation") + def test_https_proxy_set_directly(self, session: Streamlink): + # The DeprecationWarning's origin must point to this call, even without the set_option() wrapper + session.options.set("https-proxy", "https://foo") + class TestOptionsKeyEqualsValue: @pytest.fixture()