Repository: omni-us/jsonargparse
Pull request #451 (2024-02-07 22:29:31)

Fix confusing error message when creating a link with a target that is already a target of another link
## 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]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=451) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[1 New issue](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=451&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=451&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=451&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=451)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`ccf91ca`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/ccf91cac9356bc183978a12b9658a6acb34d6805?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) 100.00% compared to head [(`000c556`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451?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      #451   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         5990      5992    +2     
=========================================
+ Hits          5990      5992    +2     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/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/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.36% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.73% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.36% <0.00%> (-0.02%)` | :arrow_down: |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `48.09% <0.00%> (-0.02%)` | :arrow_down: |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.73% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.73% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.35% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.73% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.73% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `88.93% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.49% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.47% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `89.08% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.51% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.49% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.38% <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.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/451?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 27a9e1e..e3c418b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -20,6 +20,8 @@ Fixed
 - Importable class instances fail to parse and serialize (`#446
   <https://github.com/omni-us/jsonargparse/issues/446>`__).
 - Failure when trying to pickle instances created with ``lazy_instance``.
+- Confusing error message when creating a link with a target that is already a
+  target of another link.
 
 
 v4.27.4 (2024-02-01)
diff --git a/jsonargparse/_link_arguments.py b/jsonargparse/_link_arguments.py
index 709edb5..0f8a5e4 100644
--- a/jsonargparse/_link_arguments.py
+++ b/jsonargparse/_link_arguments.py
@@ -241,6 +241,8 @@ class ActionLink(Action):
             # Check source
             link_actions = self.parser._links_group._group_actions
             existing_targets = {a.target[0] for a in link_actions}
+            if target in existing_targets:
+                raise ValueError(f'Target "{target}" is already a target of another link.')
             for src in [source] if isinstance(source, str) else source:
                 if src in existing_targets:
                     raise ValueError(f'Source "{src}" not allowed since it is the target of another link.')


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index bdbe72d..b62ef11 100644
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -819,8 +819,9 @@ def test_on_instantiate_within_deeper_subclass(parser, caplog):
 def test_link_failure_invalid_apply_on(parser):
     parser.add_argument("--a")
     parser.add_argument("--b")
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError) as ctx:
         parser.link_arguments("a", "b", apply_on="bad")
+    ctx.match("apply_on must be 'parse' or 'instantiate'")
 
 
 def test_on_parse_link_failure_previous_target_as_source(parser):
@@ -845,14 +846,15 @@ def test_on_parse_link_failure_previous_source_as_target(parser):
 
 def test_on_parse_link_failure_already_linked():
     parser = parser_classes_links_on_parse()
-    with pytest.raises(ValueError):
+    with pytest.raises(ValueError) as ctx:
         parser.link_arguments("a.v2", "b.v1")
+    ctx.match('Target "b.v1" is already a target of another link')
 
 
 def test_on_parse_link_failure_non_existing_source():
     parser = parser_classes_links_on_parse()
     with pytest.raises(ValueError) as ctx:
-        parser.link_arguments("x", "b.v2")
+        parser.link_arguments("x", "b.v3")
     ctx.match('No action for key "x"')