Skip to content

Commit 363e05d

Browse files
committed
Fixed append to list not working for default_config_files #157.
1 parent a0219db commit 363e05d

File tree

7 files changed

+87
-41
lines changed

7 files changed

+87
-41
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Fixed
2222
- Failure to parse when subcommand has no options `#158
2323
<https://github.com/omni-us/jsonargparse/issues/158>`__.
2424
- Optional packages being imported even though not used.
25+
- Append to list not working for ``default_config_files`` `#157
26+
<https://github.com/omni-us/jsonargparse/issues/157>`__.
2527

2628

2729
v4.13.2 (2022-08-31)

jsonargparse/actions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def __call__(self, parser, cfg, values, option_string=None):
175175
@staticmethod
176176
def apply_config(parser, cfg, dest, value) -> None:
177177
with _ActionSubCommands.not_single_subcommand(), previous_config_context(cfg):
178-
if dest not in cfg:
178+
if cfg.get(dest) is None:
179179
cfg[dest] = []
180180
kwargs = {'env': False, 'defaults': False, '_skip_check': True, '_fail_no_subcommand': False}
181181
try:
@@ -192,6 +192,8 @@ def apply_config(parser, cfg, dest, value) -> None:
192192
cfg_file = parser.parse_path(value, **kwargs)
193193
cfg[dest].append(cfg_path)
194194
cfg.update(cfg_file)
195+
from .typehints import ActionTypeHint
196+
ActionTypeHint.apply_appends(parser, cfg)
195197

196198

197199
previous_config: ContextVar = ContextVar('previous_config', default=None)

jsonargparse/core.py

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,6 @@ def _parse_common(
259259
skip_required: bool = False,
260260
skip_subcommands: bool = False,
261261
fail_no_subcommand: bool = True,
262-
cfg_base: Optional[Namespace] = None,
263262
log_message: Optional[str] = None,
264263
) -> Namespace:
265264
"""Common parsing code used by other parse methods.
@@ -273,7 +272,6 @@ def _parse_common(
273272
skip_required: Whether to skip check of required arguments.
274273
skip_subcommands: Whether to skip subcommand processing.
275274
fail_no_subcommand: Whether to fail if no subcommand given.
276-
cfg_base: A base configuration object.
277275
log_message: Message to log at INFO level after parsing.
278276
279277
Returns:
@@ -285,18 +283,7 @@ def _parse_common(
285283
if not skip_subcommands:
286284
_ActionSubCommands.handle_subcommands(self, cfg, env=env, defaults=defaults, fail_no_subcommand=fail_no_subcommand)
287285

288-
if cfg_base is not None:
289-
cfg = self.merge_config(cfg, cfg_base)
290-
291-
if env:
292-
with _ActionPrintConfig.skip_print_config():
293-
cfg_env = self.parse_env(defaults=defaults, _skip_check=True, _skip_subcommands=True)
294-
cfg = self.merge_config(cfg, cfg_env)
295-
296-
elif defaults:
297-
cfg = self.merge_config(cfg, self.get_defaults(skip_check=True))
298-
with load_value_context(self.parser_mode):
299-
ActionTypeHint.apply_appends(self, cfg)
286+
if defaults:
300287
with lenient_check_context():
301288
ActionTypeHint.add_sub_defaults(self, cfg)
302289

@@ -317,6 +304,26 @@ def _parse_common(
317304
return cfg
318305

319306

307+
def _parse_defaults_and_environ(
308+
self,
309+
defaults: bool = True,
310+
env: Optional[bool] = None,
311+
environ: Union[Dict[str, str], os._Environ] = None,
312+
):
313+
cfg = Namespace()
314+
if defaults:
315+
cfg = self.get_defaults(skip_check=True)
316+
317+
if env or (env is None and self._default_env):
318+
if environ is None:
319+
environ = os.environ
320+
with load_value_context(self.parser_mode):
321+
cfg_env = self._load_env_vars(env=environ, defaults=defaults)
322+
cfg = self.merge_config(cfg_env, cfg)
323+
324+
return cfg
325+
326+
320327
def parse_args( # type: ignore[override]
321328
self,
322329
args: Optional[Sequence[str]] = None,
@@ -348,7 +355,11 @@ def parse_args( # type: ignore[override]
348355
argcomplete_autocomplete(self)
349356

350357
try:
351-
cfg, unk = self.parse_known_args(args=args, namespace=namespace)
358+
cfg = self._parse_defaults_and_environ(defaults, env)
359+
if namespace:
360+
cfg = self.merge_config(namespace, cfg)
361+
362+
cfg, unk = self.parse_known_args(args=args, namespace=cfg)
352363
if unk:
353364
self.error(f'Unrecognized arguments: {" ".join(unk)}')
354365

@@ -392,7 +403,12 @@ def parse_object(
392403
ParserError: If there is a parsing error and error_handler=None.
393404
"""
394405
try:
395-
cfg = self._apply_actions(cfg_obj)
406+
cfg = self._parse_defaults_and_environ(defaults, env)
407+
if cfg_base:
408+
cfg = self.merge_config(cfg_base, cfg)
409+
410+
cfg_obj = self._apply_actions(cfg_obj)
411+
cfg = self.merge_config(cfg_obj, cfg)
396412

397413
parsed_cfg = self._parse_common(
398414
cfg=cfg,
@@ -401,7 +417,6 @@ def parse_object(
401417
with_meta=with_meta,
402418
skip_check=_skip_check,
403419
skip_required=_skip_required,
404-
cfg_base=cfg_base,
405420
log_message='Parsed object.',
406421
)
407422

@@ -411,7 +426,7 @@ def parse_object(
411426
return parsed_cfg
412427

413428

414-
def _load_env_vars(self, env: Dict[str, str], defaults: bool) -> Namespace:
429+
def _load_env_vars(self, env: Union[Dict[str, str], os._Environ], defaults: bool) -> Namespace:
415430
cfg = Namespace()
416431
actions = filter_default_actions(self._actions)
417432
for action in actions:
@@ -436,10 +451,11 @@ def _load_env_vars(self, env: Dict[str, str], defaults: bool) -> Namespace:
436451
try:
437452
env_val = load_value(env_val)
438453
except get_loader_exceptions():
439-
env_val = [env_val] # type: ignore
454+
env_val = [env_val]
440455
else:
441-
env_val = [env_val] # type: ignore
456+
env_val = [env_val]
442457
cfg[action.dest] = self._check_value_key(action, env_val, action.dest, cfg)
458+
self._apply_actions(cfg)
443459
return cfg
444460

445461

@@ -465,16 +481,11 @@ def parse_env(
465481
ParserError: If there is a parsing error and error_handler=None.
466482
"""
467483
try:
468-
if env is None:
469-
env = dict(os.environ)
470-
with load_value_context(self.parser_mode):
471-
cfg = self._load_env_vars(env=env, defaults=defaults)
472-
473-
self._apply_actions(cfg)
484+
cfg = self._parse_defaults_and_environ(defaults, env=True, environ=env)
474485

475486
parsed_cfg = self._parse_common(
476487
cfg=cfg,
477-
env=False,
488+
env=True,
478489
defaults=defaults,
479490
with_meta=with_meta,
480491
skip_check=_skip_check,
@@ -562,6 +573,10 @@ def parse_string(
562573
with load_value_context(self.parser_mode):
563574
cfg = self._load_config_parser_mode(cfg_str, cfg_path, ext_vars, previous_config.get())
564575

576+
if defaults or env:
577+
cfg_base = self._parse_defaults_and_environ(defaults, env)
578+
cfg = self.merge_config(cfg, cfg_base)
579+
565580
parsed_cfg = self._parse_common(
566581
cfg=cfg,
567582
env=env,
@@ -920,19 +935,19 @@ def get_defaults(self, skip_check: bool = False) -> Namespace:
920935
cfg_file = self._load_config_parser_mode(default_config_file.get_content())
921936
if key is not None:
922937
cfg_file = cfg_file.get(key)
938+
cfg = self.merge_config(cfg_file, cfg)
923939
try:
924940
with _ActionPrintConfig.skip_print_config():
925-
cfg_file = self._parse_common(
926-
cfg=cfg_file,
927-
env=None,
941+
cfg = self._parse_common(
942+
cfg=cfg,
943+
env=False,
928944
defaults=False,
929945
with_meta=None,
930946
skip_check=skip_check,
931947
skip_required=True,
932948
)
933949
except (TypeError, KeyError, ParserError) as ex:
934950
raise ParserError(f'Problem in default config file "{default_config_file}" :: {ex.args[0]}') from ex
935-
cfg = self.merge_config(cfg_file, cfg)
936951
meta = cfg.get('__default_config__')
937952
if isinstance(meta, list):
938953
meta.append(default_config_file)
@@ -1217,6 +1232,7 @@ def merge_config(self, cfg_from: Namespace, cfg_to: Namespace) -> Namespace:
12171232
cfg = cfg_to.clone()
12181233
ActionTypeHint.discard_init_args_on_class_path_change(self, cfg, cfg_from)
12191234
cfg.update(cfg_from)
1235+
ActionTypeHint.apply_appends(self, cfg)
12201236
return cfg
12211237

12221238

jsonargparse/jsonnet.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,7 @@ def __call__(self, *args, **kwargs):
9595

9696
def _check_type(self, value, cfg):
9797
islist = _is_action_value_list(self)
98-
ext_vars = {}
99-
if self._ext_vars is not None:
100-
try:
101-
ext_vars = cfg[self._ext_vars]
102-
except KeyError:
103-
pass
98+
ext_vars = cfg.get(self._ext_vars, {})
10499
if not islist:
105100
value = [value]
106101
for num, val in enumerate(value):

jsonargparse/typehints.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
)
3333

3434
from .actions import _ActionHelpClassPath, _find_action, _find_parent_action, _is_action_value_list
35-
from .loaders_dumpers import get_loader_exceptions, load_value
35+
from .loaders_dumpers import get_loader_exceptions, load_value, load_value_context
3636
from .namespace import Namespace
3737
from .typing import is_final_class, registered_types
3838
from .optionals import (
@@ -297,7 +297,8 @@ def apply_appends(parser, cfg):
297297
for key in [k for k in cfg.keys() if k.endswith('+')]:
298298
action = _find_action(parser, key[:-1])
299299
if ActionTypeHint.supports_append(action):
300-
val = action._check_type(cfg[key], append=True, cfg=cfg)
300+
with load_value_context(parser.parser_mode):
301+
val = action._check_type(cfg[key], append=True, cfg=cfg)
301302
cfg[key[:-1]] = val
302303
cfg.pop(key)
303304

jsonargparse_tests/test_core.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,8 @@ def test_precedence_of_sources(self):
296296
cfg = parser.parse_args(['--cfg', input1_config_file])
297297
cfg_list = parser.get_config_files(cfg)
298298
self.assertEqual(default_config_file, str(cfg_list[0]))
299-
self.assertEqual(input1_config_file, str(cfg_list[1]))
299+
self.assertEqual(input2_config_file, str(cfg_list[1])) # From os.environ['APP_CFG']
300+
self.assertEqual(input1_config_file, str(cfg_list[2]))
300301

301302
for key in ['APP_CFG', 'APP_OP1']:
302303
del os.environ[key]

jsonargparse_tests/test_typehints.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,35 @@ def test_default_path_unregistered_type(self):
980980
self.assertIn('(type: Path_drw_skip_check, default: test)', out.getvalue())
981981

982982

983+
def test_list_append_default_config_files(self):
984+
config_path = pathlib.Path(self.tmpdir, 'config.yaml')
985+
parser = ArgumentParser(default_config_files=[str(config_path)])
986+
parser.add_argument('--nums', type=List[int], default=[0])
987+
988+
with self.subTest('replace in default config'):
989+
config_path.write_text('nums: [1]\n')
990+
cfg = parser.parse_args(['--nums+=2'])
991+
self.assertEqual(cfg.nums, [1, 2])
992+
cfg = parser.parse_args(['--nums+=[2, 3]'])
993+
self.assertEqual(cfg.nums, [1, 2, 3])
994+
995+
with self.subTest('append in default config'):
996+
config_path.write_text('nums+: [1]\n')
997+
cfg = parser.get_defaults()
998+
self.assertEqual(cfg.nums, [0, 1])
999+
cfg = parser.parse_args(['--nums+=2'])
1000+
self.assertEqual(cfg.nums, [0, 1, 2])
1001+
cfg = parser.parse_args(['--nums+=[2, 3]'])
1002+
self.assertEqual(cfg.nums, [0, 1, 2, 3])
1003+
1004+
with self.subTest('two default config appends'):
1005+
config_path2 = pathlib.Path(self.tmpdir, 'config2.yaml')
1006+
config_path2.write_text('nums+: [2]\n')
1007+
parser.default_config_files += [str(config_path2)]
1008+
cfg = parser.get_defaults()
1009+
self.assertEqual(cfg.nums, [0, 1, 2])
1010+
1011+
9831012
def test_class_type_with_default_config_files(self):
9841013
config = {
9851014
'class_path': 'calendar.Calendar',

0 commit comments

Comments
 (0)