Skip to content

Commit 7026eda

Browse files
authored
Run most argparse tests and document argparse deviations (#881)
1 parent 3889f58 commit 7026eda

6 files changed

Lines changed: 671 additions & 32 deletions

File tree

.github/workflows/tests.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ jobs:
5454
name: junit_py${{ matrix.python }}
5555
path: ./junit_py*
5656

57+
argparse-compatibility:
58+
runs-on: ubuntu-latest
59+
strategy:
60+
matrix:
61+
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
62+
steps:
63+
- uses: actions/checkout@v6
64+
- uses: actions/setup-python@v6
65+
with:
66+
python-version: ${{ matrix.python }}
67+
cache: pip
68+
- run: pip install tox
69+
- run: tox -e py-argparse
70+
5771
windows:
5872
runs-on: windows-2025
5973
strategy:
@@ -260,7 +274,7 @@ jobs:
260274
pypi-publish:
261275
if: startsWith(github.ref, 'refs/tags/v')
262276
runs-on: ubuntu-latest
263-
needs: [linux, windows, macos, omegaconf, pydantic-v1, installed-package, doctest, mypy]
277+
needs: [linux, windows, macos, argparse-compatibility, omegaconf, pydantic-v1, installed-package, doctest, mypy]
264278
environment:
265279
name: pypi
266280
url: https://pypi.org/p/jsonargparse

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ sphinx/_build
1111
build
1212
dist
1313
htmlcov
14+
**/tests_argparse*
1415

1516
# JetBrains IDEs
1617
.idea/

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ Changed
4040
- ``class_from_function`` and ``lazy_instance`` are now located in
4141
``jsonargparse.typing`` while the previous import locations are kept for
4242
compatibility (`#877 <https://github.com/omni-us/jsonargparse/pull/877>`__).
43+
- Most argparse tests from the Python standard library are run against
44+
jsonargparse. Also the explicit deviations from argparse are documented (`#881
45+
<https://github.com/omni-us/jsonargparse/pull/881>`__).
4346

4447

4548
v4.47.0 (2026-03-13)

DOCUMENTATION.rst

Lines changed: 112 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -383,24 +383,6 @@ will work as expected. Additionally, the following cases are also valid:
383383
--o2=val2b`` would result in ``o2=val2a``.
384384

385385

386-
Functions as type
387-
-----------------
388-
389-
Using a function as a type, like ``int_or_off`` below, is supported though
390-
discouraged. A basic requirement is that the function be idempotent, i.e.,
391-
applying the function two or more times should not modify the value. Instead of
392-
a function, it is recommended to implement a type, see :ref:`custom-types`.
393-
394-
.. testcode::
395-
396-
# either int larger than zero or 'off' string
397-
def int_or_off(x):
398-
return x if x == "off" else int(x)
399-
400-
401-
parser.add_argument("--int_or_off", type=int_or_off)
402-
403-
404386
Always fail arguments
405387
---------------------
406388

@@ -2649,17 +2631,19 @@ variables.
26492631
Subcommands
26502632
===========
26512633

2652-
One way to define parsers modularly is what in argparse is known as
2653-
`subcommands <https://docs.python.org/3/library/argparse.html#subcommands>`__.
2654-
However, to promote modularity, in jsonargparse subcommands work a bit
2655-
different from argparse. To add subcommands to a parser, the
2656-
:meth:`add_subcommands <.ArgumentParser.add_subcommands>` method is used. Then
2657-
an existing parser is added as a subcommand using :meth:`add_subcommand
2658-
<.ActionSubCommands.add_subcommand>`. In a parsed
2659-
config object the subcommand will be stored in the ``subcommand`` entry (or
2660-
whatever ``dest`` was set to), and the values of the subcommand will be in an
2661-
entry with the same name as the respective subcommand. An example of defining a
2662-
parser with subcommands is the following:
2634+
Subcommands provide a modular approach to defining parsers, similar to the
2635+
concept of `subcommands
2636+
<https://docs.python.org/3/library/argparse.html#subcommands>`__ in argparse.
2637+
However, in jsonargparse, subcommands behave somewhat differently; refer to
2638+
:ref:`argparse-deviations` for further details.
2639+
2640+
To incorporate subcommands into a parser, use the :meth:`add_subcommands
2641+
<.ArgumentParser.add_subcommands>` method. You can then add an existing parser
2642+
as a subcommand via :meth:`add_subcommand <.ActionSubCommands.add_subcommand>`.
2643+
In the resulting parsed namespace, the selected subcommand is stored under the
2644+
``subcommand`` key (or the key specified by ``dest``), and the arguments for the
2645+
subcommand are nested under a key matching the subcommand's name. The following
2646+
example demonstrates how to define a parser with subcommands:
26632647

26642648
.. testcode::
26652649

@@ -3029,6 +3013,105 @@ it as follows:
30293013
$ example.py --bool false
30303014
30313015
3016+
.. _argparse-deviations:
3017+
3018+
Deviations from argparse
3019+
========================
3020+
3021+
To ensure a high level of compatibility with argparse, the argparse tests from
3022+
the Python standard library are run against jsonargparse. Some of these tests
3023+
are skipped for the following reasons: 1) they cover intentional deviations from
3024+
argparse, 2) they are not relevant for jsonargparse, or 3) they are under
3025+
investigation and may be enabled in the future. The tests to skip are configured
3026+
in the ``argparse_tests_generate.py`` file.
3027+
3028+
The following sections describe the main intentional deviations from argparse.
3029+
In addition, deprecated features in argparse are not supported.
3030+
3031+
Subcommands
3032+
-----------
3033+
3034+
In argparse, when a parser has subcommands, the resulting namespace merges the
3035+
main parser and subparser options into a single flat namespace. Since
3036+
jsonargparse supports nested namespaces, it was a deliberate design choice to
3037+
place subcommand options in a dedicated subnamespace for greater clarity and
3038+
user convenience.
3039+
3040+
Additionally, in argparse, ``add_subparsers`` must be called with the ``dest``
3041+
parameter to include the name of the selected subcommand in the resulting
3042+
namespace. In jsonargparse, the chosen subcommand is available by default,
3043+
without requiring any extra parameters.
3044+
3045+
Furthermore, to promote modularity, subparsers in jsonargparse can be created
3046+
independently, just like the main parser. The subparser object is then added as
3047+
a subcommand. This enables defining functions that return subparsers, which can
3048+
be used both as standalone parsers and as subcommands. In contrast, argparse
3049+
subparsers are tightly coupled to the main parser and cannot be defined
3050+
independently. To avoid confusion with respect to argparse, the method names for
3051+
adding subcommands in jsonargparse are intentionally different.
3052+
3053+
To migrate from argparse to jsonargparse, instead of:
3054+
3055+
.. testcode::
3056+
3057+
import argparse
3058+
3059+
parser = argparse.ArgumentParser()
3060+
subparsers = parser.add_subparsers()
3061+
subparser1 = subparsers.add_parser("foo")
3062+
subparser1.add_argument("--key")
3063+
...
3064+
3065+
The code would be changed to:
3066+
3067+
.. testcode::
3068+
3069+
import jsonargparse
3070+
3071+
subparser1 = jsonargparse.ArgumentParser()
3072+
subparser1.add_argument("--key")
3073+
3074+
...
3075+
3076+
parser = jsonargparse.ArgumentParser()
3077+
subcommands = parser.add_subcommands()
3078+
subcommands.add_subcommand("foo", subparser1)
3079+
3080+
Parse known arguments
3081+
---------------------
3082+
3083+
Argparse provides the ``parse_known_args`` method, which allows for more lenient
3084+
parsing by ignoring unrecognized arguments. However, jsonargparse is designed
3085+
for complex parsing scenarios, such as: multiple subcommands, a large number of
3086+
arguments derived from signatures, class instantiation, and configuration files.
3087+
Allowing unrecognized arguments could make it harder for users to detect errors,
3088+
such as typos in configuration files. For this reason, jsonargparse
3089+
intentionally does not support ``parse_known_args``.
3090+
3091+
User defined types
3092+
------------------
3093+
3094+
In argparse, when adding an argument, the ``type`` parameter can be set to a
3095+
user-defined function or class. Providing a function is supported in
3096+
jsonargparse, with the additional requirement that the function must be
3097+
idempotent. That is, applying the function two or more times should not alter
3098+
the value. For example:
3099+
3100+
.. testcode::
3101+
3102+
# either int larger than zero or 'off' string
3103+
def int_or_off(x):
3104+
return x if x == "off" else int(x)
3105+
3106+
3107+
parser.add_argument("--int_or_off", type=int_or_off)
3108+
3109+
Specifying a class as the type conflicts with the signature and type hint
3110+
support that is central to jsonargparse. Therefore, providing a class as the
3111+
type does not work the same way as in argparse. The recommended alternative is
3112+
to implement a custom type; see :ref:`custom-types`.
3113+
3114+
30323115
.. _logging:
30333116

30343117
Troubleshooting and logging

0 commit comments

Comments
 (0)