Repository: omni-us/jsonargparse
Pull request #401 (2023-10-16 05:31:08)

Fix key related errors printing messages within single quotes
## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [n/a] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=401)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=401&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=401) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=401&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=401&metric=new_duplicated_lines_density&view=list)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified lines are covered by tests :white_check_mark:
> Comparison is base [(`8f5bcda`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/8f5bcdab05d22869b4569a7c0771725403890c54?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00% compared to head [(`6a13b24`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##              main      #401   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         5843      5846    +3     
=========================================
+ Hits          5843      5846    +3     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [py3.10](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.09% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.46% <50.00%> (+0.02%)` | :arrow_up: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.18% <50.00%> (+0.02%)` | :arrow_up: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.07% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.67% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.48% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.46% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.82% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.50% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.48% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.67% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.36% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.36% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Coverage Δ | |
|---|---|---|
| [jsonargparse/\_actions.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19hY3Rpb25zLnB5) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_core.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19jb3JlLnB5) | `100.00% <100.00%> (ø)` | |
| [jsonargparse/\_namespace.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19uYW1lc3BhY2UucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/401?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9604785..b899e63 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -25,6 +25,7 @@ Fixed
 ^^^^^
 - Failures with subcommands and default_config_files when keys are repeated
   (`#160 <https://github.com/omni-us/jsonargparse/issues/160>`__).
+- Key related errors printing messages within single quotes.
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py
index 32baccc..b58562b 100644
--- a/jsonargparse/_actions.py
+++ b/jsonargparse/_actions.py
@@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple, Type, Union
 
 from ._common import get_class_instantiator, is_subclass, parser_context
 from ._loaders_dumpers import get_loader_exceptions, load_value
-from ._namespace import Namespace, split_key, split_key_root
+from ._namespace import Namespace, NSKeyError, split_key, split_key_root
 from ._optionals import FilesCompleterMethod, get_config_read_mode
 from ._type_checking import ArgumentParser
 from ._util import (
@@ -715,7 +715,9 @@ class _ActionSubCommands(_SubParsersAction):
                     candidate_subcommands_str = "{" + ",".join(available_subcommands) + "}"
                 else:
                     candidate_subcommands_str = "{" + ",".join(available_subcommands[:5]) + ", ...}"
-                raise KeyError(f'expected "{dest}" to be one of {candidate_subcommands_str}, but it was not provided.')
+                raise NSKeyError(
+                    f'expected "{dest}" to be one of {candidate_subcommands_str}, but it was not provided.'
+                )
 
         return subcommand_keys, [action._name_parser_map.get(s) for s in subcommand_keys]  # type: ignore
 
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index dd162b9..7178e73 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -60,6 +60,7 @@ from ._loaders_dumpers import (
 )
 from ._namespace import (
     Namespace,
+    NSKeyError,
     is_meta_key,
     patch_namespace,
     recreate_branches,
@@ -881,7 +882,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
                 for dest, default in arg.items():
                     action = _find_action(self, dest)
                     if action is None:
-                        raise KeyError(f'No action for destination key "{dest}" to set its default.')
+                        raise NSKeyError(f'No action for key "{dest}" to set its default.')
                     elif isinstance(action, ActionConfigFile):
                         ActionConfigFile.set_default_error()
                     if isinstance(action, ActionTypeHint):
@@ -918,11 +919,11 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         """
         action, _ = _find_parent_action_and_subcommand(self, dest)
         if action is None or dest != action.dest:
-            raise KeyError(f'No action for destination key "{dest}" to get its default.')
+            raise NSKeyError(f'No action for key "{dest}" to get its default.')
 
         def check_suppressed_default():
             if action.default == argparse.SUPPRESS:
-                raise KeyError(f'Action for destination key "{dest}" does not specify a default.')
+                raise NSKeyError(f'Action for key "{dest}" does not specify a default.')
 
         if not self._get_default_config_files():
             check_suppressed_default()
@@ -1069,7 +1070,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
                 elif key in self.groups and hasattr(self.groups[key], "instantiate_class"):
                     raise TypeError(f"Class group {key!r} got an unexpected value: {val}.")
                 else:
-                    raise KeyError(f'No action for destination key "{key}" to check its value.')
+                    raise NSKeyError(f'No action for key "{key}" to check its value.')
 
         try:
             if not skip_required and not lenient_check.get():
@@ -1077,7 +1078,7 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
             with parser_context(load_value_mode=self.parser_mode):
                 check_values(cfg)
         except (TypeError, KeyError) as ex:
-            prefix = "Configuration check failed :: "
+            prefix = "Validation failed: "
             message = ex.args[0]
             if prefix not in message:
                 message = prefix + message
diff --git a/jsonargparse/_namespace.py b/jsonargparse/_namespace.py
index 929797a..47b05c7 100644
--- a/jsonargparse/_namespace.py
+++ b/jsonargparse/_namespace.py
@@ -26,6 +26,11 @@ __all__ = [
 meta_keys = {"__default_config__", "__path__", "__orig__"}
 
 
+class NSKeyError(KeyError):
+    def __str__(self):
+        return str(self.args[0])
+
+
 def split_key(key: str) -> List[str]:
     return key.split(".")
 
@@ -146,10 +151,10 @@ class Namespace(argparse.Namespace):
             KeyError: When given invalid key.
         """
         if " " in key:
-            raise KeyError(f'Spaces not allowed in keys: "{key}".')
+            raise NSKeyError(f'Spaces not allowed in keys: "{key}".')
         key_split = split_key(key)
         if any(k == "" for k in key_split):
-            raise KeyError(f'Empty nested key: "{key}".')
+            raise NSKeyError(f'Empty nested key: "{key}".')
         key_split = [add_clash_mark(k) for k in key_split]
         leaf_key = key_split[-1]
         parent_ns: Namespace = self
@@ -169,7 +174,7 @@ class Namespace(argparse.Namespace):
         """Same as _parse_key but raises KeyError if key not found."""
         leaf_key, parent_ns, parent_key = self._parse_key(key)
         if parent_ns is None or not hasattr(parent_ns, leaf_key):
-            raise KeyError(f'Key "{key}" not found in namespace.')
+            raise NSKeyError(f'Key "{key}" not found in namespace.')
         return leaf_key, parent_ns, parent_key
 
     def _create_nested_namespace(self, key: str) -> "Namespace":
@@ -306,7 +311,7 @@ class Namespace(argparse.Namespace):
         """
         if not isinstance(value, Namespace):
             if not key:
-                raise KeyError("Key is required if value not a Namespace.")
+                raise NSKeyError("Key is required if value not a Namespace.")
             if not only_unset or key not in self:
                 self[key] = value
         else:


Test patch:
diff --git a/jsonargparse_tests/test_subclasses.py b/jsonargparse_tests/test_subclasses.py
index 648394a..43872b3 100644
--- a/jsonargparse_tests/test_subclasses.py
+++ b/jsonargparse_tests/test_subclasses.py
@@ -539,7 +539,7 @@ def test_subclass_class_name_then_invalid_init_args(parser):
     parser.add_argument("--op", type=Union[Calendar, GzipFile])
     with pytest.raises(ArgumentError) as ctx:
         parser.parse_args(["--op=TextCalendar", "--op=GzipFile", "--op.firstweekday=2"])
-    ctx.match('No action for destination key "firstweekday"')
+    ctx.match('No action for key "firstweekday"')
 
 
 # dict parameter tests
@@ -1349,7 +1349,7 @@ def test_subclass_print_config(parser):
     assert obtained == {"a1": {"class_path": "calendar.Calendar", "init_args": {"firstweekday": 0}}, "a2": 7}
 
     err = get_parse_args_stderr(parser, ["--g.a1=calendar.Calendar", "--g.a1.invalid=1", "--print_config"])
-    assert 'No action for destination key "invalid"' in err
+    assert 'No action for key "invalid"' in err
 
 
 class PrintConfigRequired:
@@ -1427,7 +1427,7 @@ def test_subclass_error_unexpected_init_arg(parser):
     init_args = '"init_args": {"unexpected_arg": True}'
     with pytest.raises(ArgumentError) as ctx:
         parser.parse_args(["--op={" + class_path + ", " + init_args + "}"])
-    ctx.match('No action for destination key "unexpected_arg"')
+    ctx.match('No action for key "unexpected_arg"')
 
 
 def test_subclass_invalid_class_path_value(parser):
diff --git a/jsonargparse_tests/test_subcommands.py b/jsonargparse_tests/test_subcommands.py
index a629eb3..697108f 100644
--- a/jsonargparse_tests/test_subcommands.py
+++ b/jsonargparse_tests/test_subcommands.py
@@ -55,7 +55,7 @@ def test_subcommands_undefined_subcommand(subcommands_parser):
 
 def test_subcommands_not_given_when_few_subcommands(subcommands_parser):
     err = get_parse_args_stderr(subcommands_parser, [])
-    assert "error: 'expected \"subcommand\" to be one of {a,b,B}, but it was not provided.'" in err
+    assert 'error: expected "subcommand" to be one of {a,b,B}, but it was not provided.' in err
 
 
 def test_subcommands_not_given_when_many_subcommands(parser, subparser):
@@ -63,7 +63,7 @@ def test_subcommands_not_given_when_many_subcommands(parser, subparser):
     for subcommand in range(ord("a"), ord("l") + 1):
         subcommands.add_subcommand(chr(subcommand), subparser)
     err = get_parse_args_stderr(parser, [])
-    assert "error: 'expected \"subcommand\" to be one of {a,b,c,d,e, ...}, but it was not provided.'" in err
+    assert 'error: expected "subcommand" to be one of {a,b,c,d,e, ...}, but it was not provided.' in err
 
 
 def test_subcommands_missing_required_subargument(subcommands_parser):
@@ -121,7 +121,7 @@ def test_subcommands_parse_string_implicit_subcommand(subcommands_parser):
     assert cfg["a"] == {"ap1": "ap1_cfg", "ao1": "ao1_def"}
     with pytest.raises(ArgumentError) as ctx:
         subcommands_parser.parse_string('{"a": {"ap1": "ap1_cfg", "unk": "unk_cfg"}}')
-    ctx.match('No action for destination key "unk"')
+    ctx.match('No action for key "unk"')
 
 
 def test_subcommands_parse_string_first_implicit_subcommand(subcommands_parser):