From d5413826017c9049cb06e03cdf569acd64825312 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:44:59 +0200 Subject: [PATCH 1/2] Fix parse errors logged multiple times and document that debug logs can include secrets --- CHANGELOG.rst | 3 +++ DOCUMENTATION.rst | 4 +++- jsonargparse/_core.py | 4 +++- jsonargparse_tests/test_core.py | 9 +++++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 55adb917..8af16004 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -102,6 +102,9 @@ Fixed - A positional-only parameter that has a default being required, even though the call can omit it (`#986 `__). +- Parse errors logged multiple times when raised from a nested parse, e.g. an + invalid value in a config given as argument (`#989 + `__). Changed ^^^^^^^ diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index da13598d..9d7c817c 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -3662,7 +3662,9 @@ When a parse method fails, by default it prints a short message and exits with a non-zero code. During development this is not enough information to find the root of the problem. Setting the ``JSONARGPARSE_DEBUG`` environment variable to ``true`` changes this, without touching the source code: an -:class:`.ArgumentError` is raised and the full stack trace is printed. +:class:`.ArgumentError` is raised, the full stack trace is printed and parsers +without a logger log at debug level. Debug logs can include the raw input given +to parsers, secrets included, so only enable them for troubleshooting. The parsers log some basic events, though this is disabled by default. To enable it, set the ``logger`` argument when creating an :class:`.ArgumentParser`. The diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 5c938513..03e31439 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -1201,7 +1201,9 @@ def error(self, message: str, ex: Exception | None = None) -> NoReturn: error = argument_error(message) if source is not None: error.value_source_reported = True # type: ignore[attr-defined] # so that it is not added again - self._logger.error(message) + if not getattr(ex, "error_logged", False): # a nested error was already logged + self._logger.error(message) + error.error_logged = True # type: ignore[attr-defined] if not self.exit_on_error: raise error from ex elif debug_mode_active(): diff --git a/jsonargparse_tests/test_core.py b/jsonargparse_tests/test_core.py index d6f813b2..896357c4 100644 --- a/jsonargparse_tests/test_core.py +++ b/jsonargparse_tests/test_core.py @@ -1150,6 +1150,15 @@ def test_debug_environment_variable(logger): assert "Debug enabled, thus raising exception instead of exit" in logs.getvalue() +def test_nested_error_logged_once(logger): + parser = ArgumentParser(exit_on_error=False, logger=logger) + parser.add_argument("--cfg", action="config") + parser.add_argument("--int", type=int) + with pytest.raises(ArgumentError), capture_logs(logger) as logs: + parser.parse_args(["--cfg", '{"int": "invalid"}']) + assert 1 == logs.getvalue().count('Parser key "int"') + + def test_parse_known_args_not_implemented(parser): pytest.raises(NotImplementedError, lambda: parser.parse_known_args([])) From 15bd5fab38295eae76ec8ca946b13feaf08f25f1 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:48:46 +0200 Subject: [PATCH 2/2] Propagate parser logger to subparsers --- CHANGELOG.rst | 3 +++ jsonargparse/_core.py | 8 ++++++++ jsonargparse_tests/test_subcommands.py | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8af16004..37942160 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -105,6 +105,9 @@ Fixed - Parse errors logged multiple times when raised from a nested parse, e.g. an invalid value in a config given as argument (`#989 `__). +- Subcommand parsers not logging when the logger of the parent parser is set + after adding the subcommands (`#989 + `__). Changed ^^^^^^^ diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index 03e31439..14b305a5 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -23,6 +23,7 @@ previous_config, ) from ._common import ( + LoggerProperty, command_line_source, config_schema_key, debug_mode_active, @@ -1676,6 +1677,13 @@ def dump_header(self, dump_header: list[str] | None): raise ValueError("Expected dump_header to be None or a list of strings.") self._dump_header = dump_header + @LoggerProperty.logger.setter # type: ignore[attr-defined] + def logger(self, logger: bool | str | dict | logging.Logger): + LoggerProperty.logger.fset(self, logger) # type: ignore[attr-defined] + if self._subcommands_action: + for subparser in self._subcommands_action._name_parser_map.values(): + subparser.logger = self._logger + # Not supported methods def parse_known_args(self, *args, **kwargs) -> NoReturn: diff --git a/jsonargparse_tests/test_subcommands.py b/jsonargparse_tests/test_subcommands.py index 72b554de..858597d5 100644 --- a/jsonargparse_tests/test_subcommands.py +++ b/jsonargparse_tests/test_subcommands.py @@ -18,6 +18,7 @@ add_instantiator, ) from jsonargparse_tests.conftest import ( + capture_logs, get_parse_args_stderr, get_parse_args_stdout, get_parser_help, @@ -313,6 +314,17 @@ def test_subcommand_required_false(parser, subparser): assert cfg == Namespace(subcommand=None) +def test_subcommand_logger_set_after_adding(parser, subparser, logger): + subparser.add_argument("--int", type=int) + subcommands = parser.add_subcommands() + subcommands.add_subcommand("foo", subparser) + parser.logger = logger + assert subparser.logger is logger + with pytest.raises(ArgumentError), capture_logs(logger) as logs: + parser.parse_args(["foo", "--int=invalid"]) + assert 1 == logs.getvalue().count('Parser key "int"') + + def test_subcommand_without_options(parser, subparser): subcommands = parser.add_subcommands() subcommands.add_subcommand("foo", subparser)