diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8265720c..4b32e060 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,7 @@ Fixed ^^^^^ - Confusing error message when adding signature parameters that conflict with existing arguments. +- Deprecation warnings not printing the correct file and line of code. v4.27.0 (2023-11-02) diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 0cad3fb6..8f1d2f92 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -1467,7 +1467,7 @@ def env_prefix(self, env_prefix: Union[bool, str]): env_prefix_property_none_message, ) - deprecation_warning(ArgumentParser, env_prefix_property_none_message) + deprecation_warning(ArgumentParser, env_prefix_property_none_message, stacklevel=3) env_prefix = False elif env_prefix is True: env_prefix = os.path.splitext(self.prog)[0] diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index d486b17e..9d8843c6 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -34,7 +34,7 @@ class JsonargparseDeprecationWarning(DeprecationWarning): pass -def deprecation_warning(component, message): +def deprecation_warning(component, message, stacklevel=1): env_var = os.environ.get("JSONARGPARSE_DEPRECATION_WARNINGS", "").lower() show_warnings = env_var != "off" all_warnings = env_var == "all" @@ -49,9 +49,9 @@ def deprecation_warning(component, message): and to disable the warnings set JSONARGPARSE_DEPRECATION_WARNINGS=off. """, JsonargparseDeprecationWarning, - stacklevel=1, + stacklevel=stacklevel + 2, ) - warning(message, JsonargparseDeprecationWarning, stacklevel=3) + warning(message, JsonargparseDeprecationWarning, stacklevel=stacklevel + 2) shown_deprecation_warnings.add(component) @@ -370,7 +370,7 @@ def set_url_support(enabled: bool): def deprecation_warning_cli_return_parser(): - deprecation_warning("CLI.__init__.return_parser", cli_return_parser_message) + deprecation_warning("CLI.__init__.return_parser", cli_return_parser_message, stacklevel=2) logger_property_none_message = """ @@ -391,8 +391,8 @@ def deprecation_warning_cli_return_parser(): """ -def path_skip_check_deprecation(): - deprecation_warning("Path.__init__", path_skip_check_message) +def path_skip_check_deprecation(stacklevel=2): + deprecation_warning("Path.__init__", path_skip_check_message, stacklevel=stacklevel) path_immutable_attrs_message = """ @@ -486,8 +486,8 @@ def usage_and_exit_error_handler(parser: "ArgumentParser", message: str) -> None """ -def deprecation_warning_error_handler(): - deprecation_warning("ArgumentParser.error_handler", error_handler_message) +def deprecation_warning_error_handler(stacklevel): + deprecation_warning("ArgumentParser.error_handler", error_handler_message, stacklevel=stacklevel) class ParserDeprecations: @@ -510,7 +510,11 @@ def error_handler(self) -> Optional[Callable[["ArgumentParser", str], None]]: @error_handler.setter def error_handler(self, error_handler): if error_handler is not False: - deprecation_warning_error_handler() + stacklevel = 2 + stack = inspect.stack()[1] + if stack.filename.endswith("jsonargparse/_deprecated.py"): + stacklevel = 5 + deprecation_warning_error_handler(stacklevel) if callable(error_handler) or error_handler in {None, False}: self._error_handler = error_handler else: diff --git a/jsonargparse/_util.py b/jsonargparse/_util.py index 1e968f87..321f7d05 100644 --- a/jsonargparse/_util.py +++ b/jsonargparse/_util.py @@ -763,7 +763,7 @@ def logger(self, logger: Union[bool, str, dict, logging.Logger]): if logger is None: from ._deprecated import deprecation_warning, logger_property_none_message - deprecation_warning((LoggerProperty.logger, None), logger_property_none_message) + deprecation_warning((LoggerProperty.logger, None), logger_property_none_message, stacklevel=2) logger = False if not logger and debug_mode_active(): logger = {"level": "DEBUG"} diff --git a/jsonargparse/typing.py b/jsonargparse/typing.py index a2168372..3ccbf256 100644 --- a/jsonargparse/typing.py +++ b/jsonargparse/typing.py @@ -231,7 +231,7 @@ def path_type(mode: str, docstring: Optional[str] = None, **kwargs) -> type: if skip_check: from ._deprecated import path_skip_check_deprecation - path_skip_check_deprecation() + path_skip_check_deprecation(stacklevel=4) name += "_skip_check" key_name += " skip_check" diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py index 1b4b7f04..d9bcbde2 100644 --- a/jsonargparse_tests/test_deprecated.py +++ b/jsonargparse_tests/test_deprecated.py @@ -36,6 +36,7 @@ from jsonargparse._util import LoggerProperty, argument_error from jsonargparse_tests.conftest import ( get_parser_help, + is_posix, skip_if_docstring_parser_unavailable, skip_if_requests_unavailable, ) @@ -55,6 +56,17 @@ def suppress_stderr(): yield +source = pathlib.Path(__file__).read_text().splitlines() + + +def assert_deprecation_warn(warns, message, code): + assert message in str(warns[-1].message) + if code is None: + return + assert pathlib.Path(warns[-1].filename).name == pathlib.Path(__file__).name + assert code in source[warns[-1].lineno - 1] + + def test_deprecation_warning(): with catch_warnings(record=True) as w: message = "Deprecation warning" @@ -78,7 +90,11 @@ def test_ActionEnum(): parser = ArgumentParser(exit_on_error=False) with catch_warnings(record=True) as w: action = ActionEnum(enum=MyEnum) - assert "ActionEnum was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="ActionEnum was deprecated", + code="ActionEnum(enum=MyEnum)", + ) parser.add_argument("--enum", action=action, default=MyEnum.C, help="Description") for val in ["A", "B", "C"]: @@ -107,7 +123,11 @@ def test_ActionOperators(): parser = ArgumentParser(prog="app", exit_on_error=False) with catch_warnings(record=True) as w: parser.add_argument("--le0", action=ActionOperators(expr=("<", 0))) - assert "ActionOperators was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="ActionOperators was deprecated", + code='ActionOperators(expr=("<", 0))', + ) parser.add_argument( "--gt1.a.le4", action=ActionOperators(expr=[(">", 1.0), ("<=", 4.0)], join="and", type=float), @@ -155,7 +175,11 @@ def test_url_support_true(): assert "fr" == get_config_read_mode() with catch_warnings(record=True) as w: set_url_support(True) - assert "set_url_support was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="set_url_support was deprecated", + code="set_url_support(True)", + ) assert "fur" == get_config_read_mode() set_url_support(False) assert "fr" == get_config_read_mode() @@ -179,7 +203,11 @@ def test_instantiate_subclasses(): cfg = parser.parse_object({"cal": {"class_path": "calendar.Calendar"}}) with catch_warnings(record=True) as w: cfg_init = parser.instantiate_subclasses(cfg) - assert "instantiate_subclasses was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="instantiate_subclasses was deprecated", + code="parser.instantiate_subclasses(cfg)", + ) assert isinstance(cfg_init["cal"], Calendar) @@ -190,7 +218,11 @@ def function(a1: float): def test_single_function_cli(): with catch_warnings(record=True) as w: parser = CLI(function, return_parser=True, set_defaults={"a1": 3.4}) - assert "return_parser parameter was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="return_parser parameter was deprecated", + code="CLI(function, return_parser=True,", + ) assert isinstance(parser, ArgumentParser) @@ -205,26 +237,45 @@ def cmd2(a2: str = "X"): def test_multiple_functions_cli(): with catch_warnings(record=True) as w: parser = CLI([cmd1, cmd2], return_parser=True, set_defaults={"cmd2.a2": "Z"}) - assert "return_parser parameter was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="return_parser parameter was deprecated", + code="CLI([cmd1, cmd2], return_parser=True,", + ) assert isinstance(parser, ArgumentParser) def test_logger_property_none(): with catch_warnings(record=True) as w: LoggerProperty(logger=None) - assert " Setting the logger property to None was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message=" Setting the logger property to None was deprecated", + code="LoggerProperty(logger=None)", + ) def test_env_prefix_none(): with catch_warnings(record=True) as w: ArgumentParser(env_prefix=None) - assert "env_prefix" in str(w[-1].message) + assert_deprecation_warn( + w, + message="env_prefix", + code="ArgumentParser(env_prefix=None)", + ) def test_error_handler_parameter(): with catch_warnings(record=True) as w: parser = ArgumentParser(error_handler=usage_and_exit_error_handler) - assert "error_handler was deprecated in v4.20.0" in str(w[-1].message) + code = "ArgumentParser(error_handler=usage_" + if not is_posix: + code = None # for some reason the stack trace differs in windows + assert_deprecation_warn( + w, + message="error_handler was deprecated in v4.20.0", + code=code, + ) assert parser.error_handler == usage_and_exit_error_handler with suppress_stderr(), pytest.raises(SystemExit), catch_warnings(record=True): parser.parse_args(["--invalid"]) @@ -238,7 +289,11 @@ def custom_error_handler(self, message): parser = ArgumentParser() with catch_warnings(record=True) as w: parser.error_handler = custom_error_handler - assert "error_handler was deprecated in v4.20.0" in str(w[-1].message) + assert_deprecation_warn( + w, + message="error_handler was deprecated in v4.20.0", + code="parser.error_handler = custom_error_handler", + ) assert parser.error_handler == custom_error_handler out = StringIO() @@ -259,7 +314,11 @@ def test_parse_as_dict(tmp_cwd): f.write("{}") with catch_warnings(record=True) as w: parser = ArgumentParser(parse_as_dict=True, default_meta=False) - assert "``parse_as_dict`` parameter was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="``parse_as_dict`` parameter was deprecated", + code="ArgumentParser(parse_as_dict=True,", + ) assert {} == parser.parse_args([]) assert {} == parser.parse_env([]) assert {} == parser.parse_string("{}") @@ -283,7 +342,11 @@ def test_ActionPath(tmp_cwd): parser.add_argument("--cfg", action=ActionConfigFile) with catch_warnings(record=True) as w: parser.add_argument("--file", action=ActionPath(mode="fr")) - assert "ActionPath was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="ActionPath was deprecated", + code='ActionPath(mode="fr")', + ) parser.add_argument("--dir", action=ActionPath(mode="drw")) parser.add_argument("--files", nargs="+", action=ActionPath(mode="fr")) @@ -327,7 +390,11 @@ def test_ActionPath_skip_check(tmp_cwd): parser = ArgumentParser(exit_on_error=False) with catch_warnings(record=True) as w: parser.add_argument("--file", action=ActionPath(mode="fr", skip_check=True)) - assert "skip_check parameter of Path was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="skip_check parameter of Path was deprecated", + code='ActionPath(mode="fr", skip_check=True)', + ) cfg = parser.parse_args(["--file=not-exist"]) assert isinstance(cfg.file, Path) assert str(cfg.file) == "not-exist" @@ -395,7 +462,11 @@ def test_ActionPathList(tmp_cwd): parser = ArgumentParser(prog="app", exit_on_error=False) with catch_warnings(record=True) as w: parser.add_argument("--list", nargs="+", action=ActionPathList(mode="fr", rel="list")) - assert "ActionPathList was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="ActionPathList was deprecated", + code="ActionPathList(mode=", + ) parser.add_argument("--list_cwd", action=ActionPathList(mode="fr", rel="cwd")) cfg = parser.parse_args(["--list", list_file]) @@ -437,7 +508,11 @@ def test_import_import_docstring_parse(): with catch_warnings(record=True) as w: from jsonargparse.optionals import import_docstring_parse - assert "Only use the public API" in str(w[-1].message) + assert_deprecation_warn( + w, + message="Only use the public API", + code="from jsonargparse.optionals import import_docstring_parse", + ) assert import_docstring_parse is import_docstring_parser @@ -447,7 +522,11 @@ def test_import_from_deprecated(): with catch_warnings(record=True) as w: func = deprecated.set_url_support - assert "Only use the public API" in str(w[-1].message) + assert_deprecation_warn( + w, + message="Only use the public API", + code="func = deprecated.set_url_support", + ) assert func is set_url_support @@ -473,14 +552,22 @@ def test_import_from_module(module, attr): module = import_module(f"jsonargparse.{module}") with catch_warnings(record=True) as w: getattr(module, attr) - assert "Only use the public API" in str(w[-1].message) + assert_deprecation_warn( + w, + message="Only use the public API", + code="getattr(module, attr)", + ) @pytest.mark.skipif(not jsonnet_support, reason="jsonnet package is required") def test_action_jsonnet_ext_vars(parser): with catch_warnings(record=True) as w: parser.add_argument("--ext_vars", action=ActionJsonnetExtVars()) - assert "ActionJsonnetExtVars was deprecated" in str(w[-1].message) + assert_deprecation_warn( + w, + message="ActionJsonnetExtVars was deprecated", + code="action=ActionJsonnetExtVars()", + ) parser.add_argument("--jsonnet", action=ActionJsonnet(ext_vars="ext_vars")) cfg = parser.parse_args(["--ext_vars", '{"param": 123}', "--jsonnet", example_2_jsonnet])