Repository: omni-us/jsonargparse
Pull request #687 (2025-03-03 02:08:42)

Fix/type action restriction
<!--
Thank you very much for contributing! If you like this project, please ⭐ star it
https://github.com/omni-us/jsonargparse/
-->

## What does this PR do?

Fix https://github.com/omni-us/jsonargparse/issues/685
Now in `add_argument`, passing `action=None` behaves like call without argument `action`.

```py3
parser.add_argument(
    "--string",
    type=str,
    action=None,
    default="default",
    help="example string",
)
```
<!--
Concisely describe what this pull request does. If available, include links to
issues web pages where the need for this change has been motivated.
-->

## Before submitting

<!--
Use the following list as tasks to be completed before marking a pull request as
"Ready for review". Fill with an "x" all tasks done. Use "n/a" if you think a
task is not relevant or leave empty when in doubt.
-->

- [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)


PabloLION: ## Additional info
When creating remote branch, the git hook fired and I got this

```sh
mypy................................................................................Failed
- hook id: mypy
- duration: 0.98s
- exit code: 1

jsonargparse/_stubs_resolver.py:51: error: No overload variant of "Assign" matches argument types "list[Name | Attribute | Subscript]", "expr | None", "list[Never]", "int", "int"  [call-overload]
jsonargparse/_stubs_resolver.py:51: note: Possible overload variants:
jsonargparse/_stubs_resolver.py:51: note:     def Assign(self, targets: list[expr], value: expr, type_comment: str | None = ..., *, lineno: int = ..., col_offset: int = ..., end_lineno: int | None = ..., end_col_offset: int | None = ...) -> Assign
jsonargparse/_stubs_resolver.py:51: note:     def Assign(self, targets: list[expr] = ..., *, value: expr, type_comment: str | None = ..., lineno: int = ..., col_offset: int = ..., end_lineno: int | None = ..., end_col_offset: int | None = ...) -> Assign
jsonargparse/_stubs_resolver.py:53: error: Unused "type: ignore" comment  [unused-ignore]
jsonargparse/_stubs_resolver.py:54: error: Unused "type: ignore" comment  [unused-ignore]
Found 3 errors in 1 file (checked 3 source files)
```

Since I didn't edit these files, I used `--no-verify`.

Also, I didn't have mypy/ruff installed (which made. Maybe we can consider adding them to the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)? I saw them in `pyproject.toml` but I could format my code :-( without them.


PabloLION: not sure where to put the test:

- test_typehints (I added here: the edited file is _typehints.py, and the feature is also about a valid `type` with a `None` action)
- test_actions (the feature is about `action`)
- test_core (the feature is about `add_argument`)
- test_formatters (do we need to test how help changes? like adding "test_help_type_and_no_action")
PabloLION: Also I find another problem: we pass in `choices=None` it works differently here
https://github.com/omni-us/jsonargparse/blob/c3d3df7bee6b606616ee8f3ac41d09a05a2ac90d/jsonargparse/_core.py#L142

Should I create another PR on this?

mauvilsa: > When creating remote branch, the git hook fired and I got this
> 
> ```shell
> mypy................................................................................Failed
> ```

I see. I think I need to specify in the pre-commit config which python version to run mypy with. No problem that you skipped it.

> Also, I didn't have mypy/ruff installed (which made. Maybe we can consider adding them to the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)? I saw them in `pyproject.toml` but I could format my code :-( without them.

mypy/ruff don't need to be installed. It is `pre-commit` the one that installs them in a separate environment. If you want to only run `ruff` do
```shell
pre-commit run -a ruff
```
to only run `mypy`, do
```shell
pre-commit run -a --hook-stage pre-push mypy
```
PabloLION: > > Also I find another problem: we pass in `choices=None` it works differently here
> > https://github.com/omni-us/jsonargparse/blob/c3d3df7bee6b606616ee8f3ac41d09a05a2ac90d/jsonargparse/_core.py#L142
> > 
> > Should I create another PR on this?
> 
> How does it work differently? I think it could be part of this pull request.

When `action is not None`, passing `choices=None`, there will be similar problem with `type=None`.
But I have more doubts about choices: I'm creating a `parser` with `dataclass` where some fields are Enum type, and only a part of the Enum should be used. (Enum CLI args are matched by the name instead of value.)
In this case, jsonargparse will check if the parsed value is in `choices`, not the user input. However, the help test is only showing the parsed option, not what user should input.

I meant to create a new issue but I have not read the document nor have I tested it yet. Do you think this worths a shot?
PabloLION: I added the change log and I think it's ready to merge.

> Can you explain what you mean by "I cannot format the code effectively"? The code will be automatically formatted when pre-commit runs. And even if pre-commit is skipped, a github action will format it.

I wanted to say that I avoided formatting the code intentionally because my line length was set to 80 instead of 120 on my local environment because I didn't know about the GitHub action. And Pylance was reporting this everywhere

```text
"_ActionPrintConfig" is private and used outside of the module in which it is declared (Pylance) 
[reportPrivateUsage](https://github.com/microsoft/pyright/blob/main/docs/configuration.md#reportPrivateUsage)
```

And about the Union type deprecation "This type is deprecated as of Python 3.10" etc. I guess our env was very different and I didn't change my python interpreter version (I'm exactly suffering for "mac313").

Anyway, thanks for your patience and guidance in this PR. I'll create another issue on the "choices" args.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/687?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 [(`c3d3df7`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/c3d3df7bee6b606616ee8f3ac41d09a05a2ac90d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`0fbc4c4`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/0fbc4c4a8c6aaf2de6d3498e3ed18d65bb8e1e11?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      #687   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           22        22           
  Lines         6688      6688           
=========================================
  Hits          6688      6688           
```

</details>

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


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a4bf72b..0ca5658 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -28,7 +28,8 @@ Fixed
 - Regression parsing strings with omegaconf as the parser mode (`#686
   <https://github.com/omni-us/jsonargparse/pull/686>`__).
 - Help incorrectly showing environment variable name for ``--print_shtab``.
-
+- ``add_argument`` raises error when type is assigned with ``action=None``
+  (`#687 <https://github.com/omni-us/jsonargparse/issues/687>`__).
 
 v4.37.0 (2025-02-14)
 --------------------
diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py
index 564a676..1a950a6 100644
--- a/jsonargparse/_core.py
+++ b/jsonargparse/_core.py
@@ -122,7 +122,7 @@ class ActionsContainer(SignatureArguments, argparse._ActionsContainer):
             enable_path: Whether to try parsing path/subconfig when argument is a complex type.
         """
         parser = self.parser if hasattr(self, "parser") else self
-        if "action" in kwargs:
+        if kwargs.get("action") is not None:
             if ActionParser._is_valid_action_parser(parser, kwargs["action"]):
                 return ActionParser._move_parser_actions(parser, args, kwargs)
             ActionConfigFile._ensure_single_config_argument(self, kwargs["action"])
diff --git a/jsonargparse/_typehints.py b/jsonargparse/_typehints.py
index e120f7a..3b52991 100644
--- a/jsonargparse/_typehints.py
+++ b/jsonargparse/_typehints.py
@@ -280,7 +280,7 @@ class ActionTypeHint(Action):
 
     @staticmethod
     def prepare_add_argument(args, kwargs, enable_path, container, logger, sub_add_kwargs=None):
-        if "action" in kwargs:
+        if kwargs.get("action") is not None:
             raise ValueError("Providing both type and action not allowed.")
         typehint = kwargs.pop("type")
         if args[0].startswith("--") and ActionTypeHint.supports_append(typehint):


Test patch:
diff --git a/jsonargparse_tests/test_typehints.py b/jsonargparse_tests/test_typehints.py
index be360b9..d08601d 100644
--- a/jsonargparse_tests/test_typehints.py
+++ b/jsonargparse_tests/test_typehints.py
@@ -68,6 +68,11 @@ def test_add_argument_failure_given_type_and_action(parser):
     ctx.match("Providing both type and action not allowed")
 
 
+def test_add_argument_given_type_and_null_action(parser):
+    parser.add_argument("--op1", type=Optional[bool], action=None)
+    assert parser.get_defaults().op1 is None
+
+
 @pytest.mark.parametrize("typehint", [Namespace, Optional[Namespace], Union[int, Namespace], List[Namespace]])
 def test_namespace_unsupported_as_type(parser, typehint):
     with pytest.raises(ValueError, match="Namespace .* not supported as a type"):