Skip to content

Commit 6530146

Browse files
committed
Multiple subcommand settings without explicit subcommand is now a warning instead of exception.
1 parent 2f307ee commit 6530146

File tree

4 files changed

+39
-13
lines changed

4 files changed

+39
-13
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ only be introduced in major versions with advance notice in the **Deprecated**
1010
section of releases.
1111

1212

13+
v3.19.0 (2021-??-??)
14+
--------------------
15+
16+
Changed
17+
^^^^^^^
18+
- Multiple subcommand settings without explicit subcommand is now a warning
19+
instead of exception.
20+
21+
1322
v3.18.0 (2021-08-18)
1423
--------------------
1524

jsonargparse/actions.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"""Collection of useful actions to define arguments."""
22

3+
import argparse
34
import os
45
import re
56
import sys
7+
import warnings
68
import yaml
7-
import argparse
8-
from typing import Callable, Tuple, Type, Union
99
from argparse import Namespace, Action, SUPPRESS, _HelpAction, _SubParsersAction
10+
from typing import Callable, Tuple, Type, Union
1011

1112
from .optionals import FilesCompleterMethod, get_config_read_mode
1213
from .typing import path_type
@@ -746,24 +747,30 @@ def get_subcommand(parser, cfg_dict:dict, prefix:str='', fail_no_subcommand:bool
746747
if isinstance(action, _ActionSubCommands):
747748
break
748749

749-
# Get sub-command parser
750+
# Get subcommand settings keys
750751
subcommand_keys = []
751752
for key in action.choices.keys():
752753
if any([k.startswith(prefix+key+'.') for k in cfg_dict.keys()]):
753754
subcommand_keys.append(key)
754755

756+
# Get subcommand
755757
subcommand = None
756758
dest = prefix + action.dest
757759
if dest in cfg_dict and cfg_dict[dest] is not None:
758760
subcommand = cfg_dict[dest]
761+
elif len(subcommand_keys) > 0:
762+
cfg_dict[dest] = subcommand = subcommand_keys[0]
763+
if len(subcommand_keys) > 1:
764+
warnings.warn(
765+
'Multiple subcommand settings provided (' + ', '.join(subcommand_keys) + ') without an explicit "' +
766+
dest + '" key. Subcommand "' + subcommand + '" will be used.'
767+
)
768+
769+
# Remove extra subcommand settings
770+
if subcommand and len(subcommand_keys) > 1:
759771
for key in [k for k in subcommand_keys if k != subcommand]:
760772
for subkey in [k for k in cfg_dict.keys() if k.startswith(prefix+key+'.')]:
761773
del cfg_dict[subkey]
762-
else:
763-
if len(subcommand_keys) > 1:
764-
raise KeyError('Key "'+dest+'" is required if multiple sub-command values are provided: '+str(subcommand_keys)+'.')
765-
elif len(subcommand_keys) == 1:
766-
cfg_dict[dest] = subcommand = subcommand_keys[0]
767774

768775
if subcommand is None and not (fail_no_subcommand and action._required):
769776
return None, None

jsonargparse_tests/core_tests.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env python3
22

3-
import sys
43
import json
5-
import yaml
64
import platform
5+
import warnings
6+
import yaml
77
from io import StringIO
88
from contextlib import redirect_stdout
99
from collections import OrderedDict
@@ -361,9 +361,19 @@ def test_subcommands(self):
361361
self.assertEqual(cfg['subcommand'], 'a')
362362
self.assertEqual(strip_meta(cfg['a']), {'ap1': 'ap1_cfg', 'ao1': 'ao1_def'})
363363
self.assertRaises(ParserError, lambda: parser.parse_string('{"a": {"ap1": "ap1_cfg", "unk": "unk_cfg"}}'))
364-
self.assertRaises(ParserError, lambda: parser.parse_string('{"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}'))
365364

366-
cfg = parser.parse_string('{"subcommand": "a", "a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}')
365+
with warnings.catch_warnings(record=True) as w:
366+
warnings.simplefilter("always")
367+
cfg = parser.parse_string('{"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}')
368+
self.assertEqual(cfg.subcommand, 'a')
369+
self.assertFalse(hasattr(cfg, 'b'))
370+
self.assertEqual(len(w), 1)
371+
self.assertIn('Subcommand "a" will be used', str(w[0].message))
372+
373+
cfg = parser.parse_string('{"subcommand": "b", "a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}')
374+
self.assertFalse(hasattr(cfg, 'a'))
375+
376+
cfg = parser.parse_args(['--cfg={"a": {"ap1": "ap1_cfg"}, "b": {"nums": {"val1": 2}}}', 'a'])
367377
self.assertFalse(hasattr(cfg, 'b'))
368378

369379
os.environ['APP_O1'] = 'o1_env'

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ long_description_content_type = text/x-rst
6363
author = Mauricio Villegas
6464
author_email = [email protected]
6565
license = MIT
66-
url = https://omni-us.github.io/jsonargparse
66+
url = https://github.com/omni-us/jsonargparse
6767
project_urls =
6868
Documentation-stable = https://jsonargparse.readthedocs.io/en/stable/
6969
Documentation-latest = https://jsonargparse.readthedocs.io/en/latest/

0 commit comments

Comments
 (0)