Repository: omni-us/jsonargparse
Pull request #505 (2024-05-21 04:59:51)

 Support implicit class_path when given a dict or single parameter
## What does this PR do?

Directly providing a dict with parameters or a single parameter to a subclass or callable with class return now implicitly tries using the base class as class_path if not abstract.

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [x] 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=505) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=505&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=505&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=505&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=505&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=505)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505?dropdown=coverage&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:
> Project coverage is 99.78%. Comparing base [(`8c26223`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/8c26223e43fca54414daf5f978d1e21aeb8a9747?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`3e2198f`)](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).


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


```diff
@@             Coverage Diff             @@
##              main     #505      +/-   ##
===========================================
- Coverage   100.00%   99.78%   -0.22%     
===========================================
  Files           21       21              
  Lines         6128     6139      +11     
===========================================
- Hits          6128     6126       -2     
- Misses           0       13      +13     
```

| [Flag](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/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/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.10_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.10_pydantic1](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.10_pydantic2](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.10_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `?` | |
| [py3.11](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/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/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.64% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.11_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.66% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.40% <100.00%> (+0.02%)` | :arrow_up: |
| [py3.12_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.64% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.12_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `98.66% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.82% <100.00%> (+0.02%)` | :arrow_up: |
| [py3.7_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.26% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.7_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.26% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.99% <100.00%> (+0.02%)` | :arrow_up: |
| [py3.8_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.41% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.8_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.41% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `87.82% <100.00%> (+0.02%)` | :arrow_up: |
| [py3.9_all](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.28% <100.00%> (+<0.01%)` | :arrow_up: |
| [py3.9_types](https://app.codecov.io/gh/omni-us/jsonargparse/pull/505/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | `99.29% <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/505?dropdown=coverage&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).

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=505) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=505&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=505&resolutions=WONTFIX)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=505&resolved=false&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/no-data-16px.png '') No data about Coverage  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=505&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=505)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6635a64..8e61671 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -19,6 +19,9 @@ Added
 ^^^^^
 - Support for ``TypedDict`` (`#457
   <https://github.com/omni-us/jsonargparse/issues/457>`__).
+- Directly providing a dict with parameters or a single parameter to a subclass
+  or callable with class return now implicitly tries using the base class as
+  ``class_path`` if not abstract.
 
 Fixed
 ^^^^^
diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst
index f402745..d11af8d 100644
--- a/DOCUMENTATION.rst
+++ b/DOCUMENTATION.rst
@@ -2013,6 +2013,16 @@ been imported before parsing. Abstract classes and private classes (module or
 name starting with ``'_'``) are not considered. All the subclasses resolvable by
 its name can be seen in the general help ``python tool.py --help``.
 
+When the base class is not abstract, the ``class_path`` can be omitted, by
+giving directly ``init_args``, for example:
+
+.. code-block:: bash
+
+    python tool.py --calendar.firstweekday 2
+
+would implicitly use ``calendar.Calendar`` as the class path.
+
+
 Default values
 --------------
 
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index 703a724..0e845b4 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -313,7 +313,10 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, argp
         _ActionPrintConfig.print_config_if_requested(self, cfg)
 
         with parser_context(parent_parser=self):
-            ActionLink.apply_parsing_links(self, cfg)
+            try:
+                ActionLink.apply_parsing_links(self, cfg)
+            except Exception as ex:
+                self.error(str(ex), ex)
 
             if not skip_check and not lenient_check.get():
                 self.check_config(cfg, skip_required=skip_required)
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index c2ad0ba..f90c44e 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -865,6 +865,11 @@ def adapt_typehints(
                     else:
                         raise ImportError(f"Unexpected import object {val_obj}")
                 if isinstance(val, (dict, Namespace, NestedArg)):
+                    if prev_val is None:
+                        return_type = get_callable_return_type(typehint)
+                        if return_type and not inspect.isabstract(return_type):
+                            with suppress(ValueError):
+                                prev_val = Namespace(class_path=get_import_path(return_type))
                     val = subclass_spec_as_namespace(val, prev_val)
                     if not is_subclass_spec(val):
                         raise ImportError(
@@ -920,6 +925,9 @@ def adapt_typehints(
             return val
 
         val_input = val
+        if prev_val is None and not inspect.isabstract(typehint):
+            with suppress(ValueError):
+                prev_val = Namespace(class_path=get_import_path(typehint))
         val = subclass_spec_as_namespace(val, prev_val)
         if not is_subclass_spec(val):
             raise_unexpected_value(


Test patch:
diff --git a/jsonargparse_tests/test_link_arguments.py b/jsonargparse_tests/test_link_arguments.py
index b62ef11..54f2744 100644
--- a/jsonargparse_tests/test_link_arguments.py
+++ b/jsonargparse_tests/test_link_arguments.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 from calendar import Calendar, TextCalendar
 from dataclasses import dataclass
+from importlib.util import find_spec
 from typing import Any, Callable, List, Mapping, Optional, Union
 
 import pytest
@@ -51,7 +52,7 @@ def test_on_parse_subcommand_failing_compute_fn(parser, subparser, subtests):
     subcommands.add_subcommand("sub", subparser)
 
     with subtests.test("parse_args"):
-        with pytest.raises(ValueError) as ctx:
+        with pytest.raises(ArgumentError) as ctx:
             parser.parse_args(["sub"])
         ctx.match("Call to compute_fn of link 'to_str.*failed: value is empty")
 
@@ -111,7 +112,10 @@ def test_on_parse_compute_fn_subclass_spec(parser, subtests):
         parser.set_defaults(cal1=None)
         with pytest.raises(ArgumentError) as ctx:
             parser.parse_args(["--cal1.firstweekday=-"])
-        ctx.match('Parser key "cal1"')
+        if find_spec("typeshed_client"):
+            ctx.match('Parser key "cal1"')
+        else:
+            ctx.match("Call to compute_fn of link")
 
 
 class ClassA:
diff --git a/jsonargparse_tests/test_subclasses.py b/jsonargparse_tests/test_subclasses.py
index 3f2f6d4..fb2275b 100644
--- a/jsonargparse_tests/test_subclasses.py
+++ b/jsonargparse_tests/test_subclasses.py
@@ -246,13 +246,6 @@ def test_subclass_init_args_without_class_path(parser):
     assert cfg.cal3.init_args == Namespace(firstweekday=5)
 
 
-def test_subclass_init_args_without_class_path_error(parser):
-    parser.add_subclass_arguments(Calendar, "cal1")
-    with pytest.raises(ArgumentError) as ctx:
-        parser.parse_args(["--cal1.init_args.firstweekday=4"])
-    ctx.match("class path given previously")
-
-
 def test_subclass_init_args_without_class_path_dict(parser):
     parser.add_argument("--cfg", action=ActionConfigFile)
     parser.add_argument("--cal", type=Calendar)
@@ -1500,6 +1493,23 @@ def test_subclass_help_not_subclass(parser):
     ctx.match("is not a subclass of")
 
 
+class Implicit:
+    def __init__(self, a: int = 1, b: str = ""):
+        pass
+
+
+def test_subclass_implicit_class_path(parser):
+    parser.add_argument("--implicit", type=Implicit)
+    cfg = parser.parse_args(['--implicit={"a": 2, "b": "x"}'])
+    assert cfg.implicit.class_path == f"{__name__}.Implicit"
+    assert cfg.implicit.init_args == Namespace(a=2, b="x")
+    cfg = parser.parse_args(["--implicit.a=3"])
+    assert cfg.implicit.init_args == Namespace(a=3, b="")
+    with pytest.raises(ArgumentError) as ctx:
+        parser.parse_args(['--implicit={"c": null}'])
+    ctx.match('No action for key "c" to check its value')
+
+
 # error messages tests
 
 
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index 5e6ce9c..e4de2ec 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -754,6 +754,16 @@ def test_callable_args_return_type_class(parser, subtests):
             assert f"{__name__}.{name}" in help_str
 
 
+def test_callable_return_type_class_implicit_class_path(parser):
+    parser.add_argument("--optimizer", type=Callable[[List[float]], Optimizer])
+    cfg = parser.parse_args(['--optimizer={"lr": 0.5}'])
+    assert cfg.optimizer.class_path == f"{__name__}.Optimizer"
+    assert cfg.optimizer.init_args == Namespace(lr=0.5, momentum=0.0)
+    cfg = parser.parse_args(["--optimizer.momentum=0.2"])
+    assert cfg.optimizer.class_path == f"{__name__}.Optimizer"
+    assert cfg.optimizer.init_args == Namespace(lr=0.001, momentum=0.2)
+
+
 def test_callable_multiple_args_return_type_class(parser, subtests):
     parser.add_argument("--optimizer", type=Callable[[List[float], float], Optimizer], default=SGD)
 
@@ -924,7 +934,7 @@ class Model(Module):
         self.activation = activation
 
 
-def test_callable_zero_args_return_type_class(parser):  # , subtests):
+def test_callable_zero_args_return_type_class(parser):
     parser.add_class_arguments(Model, "model")
     cfg = parser.parse_args([])
     assert cfg.model.activation == Namespace(