Repository: omni-us/jsonargparse
Pull request #701 (2025-04-04 05:41:14)

auto_cli now supports class @property
## What does this PR do?

See https://github.com/omni-us/jsonargparse/issues/438#issuecomment-2765311380

## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [ ] 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)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/701?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 100.00%. Comparing base [(`aac2bd1`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/aac2bd182adc80ca1e03edf632f7216f0f5cafca?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`b2ab93d`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/b2ab93dc0a1ca8c57596bb3d4fed7e3fae067e38?dropdown=coverage&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      #701   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines         6765      6768    +3     
=========================================
+ Hits          6765      6768    +3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/701?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).

sonarqubecloud[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=701) **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=701&issueStatuses=OPEN,CONFIRMED&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=701&issueStatuses=ACCEPTED)

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=701&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=701&metric=new_coverage&view=list)  
![](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=701&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=701)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5878355..feef512 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -21,6 +21,8 @@ Added
   <https://github.com/omni-us/jsonargparse/pull/699>`__).
 - ``shtab`` support for optionals parsed as positionals (`#700
   <https://github.com/omni-us/jsonargparse/pull/700>`__).
+- ``auto_cli`` now supports class ``@property`` (`#701
+  <https://github.com/omni-us/jsonargparse/pull/701>`__).
 
 Changed
 ^^^^^^^
diff --git a/jsonargparse/_cli.py b/jsonargparse/_cli.py
index 7335256..32953bb 100644
--- a/jsonargparse/_cli.py
+++ b/jsonargparse/_cli.py
@@ -157,7 +157,7 @@ def _add_subcommands(
                 remove_actions(subparser, (ActionConfigFile, _ActionPrintConfig))
 
 
-def has_parameter(component, name):
+def has_parameter(component, name) -> bool:
     return name in inspect.signature(component).parameters.keys()
 
 
@@ -170,7 +170,9 @@ def _add_component_to_parser(
 ):
     kwargs: dict = dict(as_positional=as_positional, fail_untyped=fail_untyped, sub_configs=True)
     if inspect.isclass(component):
-        class_methods = [k for k, v in inspect.getmembers(component) if callable(v) and k[0] != "_"]
+        class_methods = [
+            k for k, v in inspect.getmembers(component) if (callable(v) or isinstance(v, property)) and k[0] != "_"
+        ]
         if not class_methods:
             added_args = parser.add_class_arguments(component, as_group=False, **kwargs)
             if not parser.description:
@@ -182,12 +184,13 @@ def _add_component_to_parser(
             method_object = getattr(component, method)
             description = get_help_str(method_object, parser.logger)
             subparser = type(parser)(description=description)
-            if not has_parameter(getattr(component, method), "config"):
-                subparser.add_argument("--config", action=ActionConfigFile, help=config_help)
-            added_subargs = subparser.add_method_arguments(component, method, as_group=False, **kwargs)
-            added_args += [f"{method}.{a}" for a in added_subargs]
-            if not added_subargs:
-                remove_actions(subparser, (ActionConfigFile, _ActionPrintConfig))
+            if not isinstance(method_object, property):
+                if not has_parameter(method_object, "config"):
+                    subparser.add_argument("--config", action=ActionConfigFile, help=config_help)
+                added_subargs = subparser.add_method_arguments(component, method, as_group=False, **kwargs)
+                added_args += [f"{method}.{a}" for a in added_subargs]
+                if not added_subargs:
+                    remove_actions(subparser, (ActionConfigFile, _ActionPrintConfig))
             subcommands.add_subcommand(method, subparser, help=get_help_str(method_object, parser.logger))
     else:
         added_args = parser.add_function_arguments(component, as_group=False, **kwargs)
@@ -203,6 +206,8 @@ def _run_component(component, cfg):
         subcommand_cfg = cfg.pop(subcommand, {})
         subcommand_cfg.pop("config", None)
         component_obj = component(**cfg)
+        if isinstance(getattr(component, subcommand), property):
+            return getattr(component_obj, subcommand)
         component = getattr(component_obj, subcommand)
         cfg = subcommand_cfg
     if inspect.iscoroutinefunction(component):


Test patch:
diff --git a/jsonargparse_tests/test_cli.py b/jsonargparse_tests/test_cli.py
index 507cd3f..ea860fa 100644
--- a/jsonargparse_tests/test_cli.py
+++ b/jsonargparse_tests/test_cli.py
@@ -237,6 +237,25 @@ def test_method_with_config_parameter():
     assert "p1: 1, config: {}" == out.strip()
 
 
+class WithProperty:
+    def __init__(self, num: int = 1):
+        self._num = num
+
+    @property
+    def prop(self):
+        """Description of property"""
+        return self._num + 1
+
+
+def test_class_property_value():
+    assert 3 == auto_cli(WithProperty, args=["--num=2", "prop"])
+    if docstring_parser_support:
+        help_str = get_cli_stdout(WithProperty, args=["--help"])
+        assert "Description of property" in help_str
+        help_str = get_cli_stdout(WithProperty, args=["prop", "--help"])
+        assert "Description of property" in help_str
+
+
 # function and class tests