Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ Deprecated
- Implicit selection of subcommand when multiple subcommand settings available
is deprecated and will be removed in v5.0.0. Provide an explicit subcommand
instead (`#923 <https://github.com/omni-us/jsonargparse/pull/923>`__).
- ``ArgumentParser.merge_config`` is deprecated and will be removed in v5.0.0.
There is no replacement since it is considered internal (`#925
<https://github.com/omni-us/jsonargparse/pull/925>`__).


v4.49.0 (2026-05-15)
Expand Down
7 changes: 4 additions & 3 deletions jsonargparse/_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
indent_text,
iter_to_set_str,
load_config_path_context,
merge_config,
parse_value_or_config,
)

Expand Down Expand Up @@ -127,7 +128,7 @@ def apply_config(parser, cfg, dest, value) -> None:
raise TypeError(f'Parser key "{dest}": {ex_str}') from ex_str
else:
cfg_file = parser.parse_path(value, **kwargs)
cfg_merged = parser.merge_config(cfg_file, cfg)
cfg_merged = merge_config(parser, cfg_file, cfg)
cfg.__dict__.update(cfg_merged.__dict__)
if cfg.get(dest) is get_parsing_setting("unset_sentinel"):
cfg[dest] = []
Expand Down Expand Up @@ -246,8 +247,8 @@ def __call__(self, *args, **kwargs):
parser, namespace, value = args[:3]
loaded_value = self._load_config(value, parser)
if isinstance(namespace.get(self.dest), Namespace):
loaded_value = parser.merge_config(
Namespace({self.dest: loaded_value}), Namespace({self.dest: namespace[self.dest]})
loaded_value = merge_config(
parser, Namespace({self.dest: loaded_value}), Namespace({self.dest: namespace[self.dest]})
)[self.dest]
namespace[self.dest] = loaded_value
return None
Expand Down
4 changes: 2 additions & 2 deletions jsonargparse/_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
is_subclass,
type_to_str,
)
from ._util import NoneType, Path, import_object, unique
from ._util import NoneType, Path, import_object, merge_config, unique


def handle_completions(parser):
Expand Down Expand Up @@ -80,7 +80,7 @@ def parse_known_args(self, args=None, namespace=None):

def get_argcomplete_namespace(parser, namespace):
namespace.__class__ = __import__("jsonargparse").Namespace
return parser.merge_config(parser.get_defaults(skip_validation=True), namespace).as_flat()
return merge_config(parser, parser.get_defaults(skip_validation=True), namespace).as_flat()


def get_files_completer():
Expand Down
34 changes: 9 additions & 25 deletions jsonargparse/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
get_private_kwargs,
identity,
load_config_path_context,
merge_config,
return_parser_if_captured,
)

Expand Down Expand Up @@ -409,7 +410,7 @@ def _parse_defaults_and_environ(
environ = os.environ
with parser_context(load_value_mode=self.parser_mode):
cfg_env = self._load_env_vars(env=environ, defaults=defaults)
cfg = self.merge_config(cfg_env, cfg)
cfg = merge_config(self, cfg_env, cfg)

return cfg

Expand Down Expand Up @@ -457,13 +458,13 @@ def parse_args( # type: ignore[override]
if namespace:
if namespace_as_config:
cfg = self._parse_defaults_and_environ(defaults, env=False)
cfg = self.merge_config(namespace, cfg)
cfg = merge_config(self, namespace, cfg)
if env or (env is None and self._default_env):
with parser_context(load_value_mode=self.parser_mode):
cfg_env = self._load_env_vars(env=os.environ, defaults=defaults)
cfg = self.merge_config(cfg_env, cfg)
cfg = merge_config(self, cfg_env, cfg)
else:
cfg = self.merge_config(namespace, cfg)
cfg = merge_config(self, namespace, cfg)

with parse_kwargs_context({"env": env, "defaults": defaults}):
cfg, unk = self._parse_known_args_internal(args=args, namespace=cfg)
Expand Down Expand Up @@ -510,11 +511,11 @@ def parse_object(
try:
cfg = self._parse_defaults_and_environ(defaults, env)
if cfg_base:
cfg = self.merge_config(cfg_base, cfg)
cfg = merge_config(self, cfg_base, cfg)

cfg = self._apply_actions(cfg)
cfg_apply = self._apply_actions(cfg_obj, prev_cfg=cfg)
cfg = self.merge_config(cfg_apply, cfg)
cfg = merge_config(self, cfg_apply, cfg)

parsed_cfg = self._parse_common(
cfg=cfg,
Expand Down Expand Up @@ -681,7 +682,7 @@ def parse_string(

if defaults or env:
cfg_base = self._parse_defaults_and_environ(defaults, env)
cfg = self.merge_config(cfg, cfg_base)
cfg = merge_config(self, cfg, cfg_base)

parsed_cfg = self._parse_common(
cfg=cfg,
Expand Down Expand Up @@ -1059,7 +1060,7 @@ def get_defaults(self, skip_validation: bool = False, **kwargs) -> Namespace:
if not default_config_file_content.strip():
continue
cfg_file = self._load_config_parser_mode(default_config_file_content, prev_cfg=cfg)
cfg = self.merge_config(cfg_file, cfg)
cfg = merge_config(self, cfg_file, cfg)
try:
with _ActionPrintConfig.skip_print_config():
cfg = self._parse_common(
Expand Down Expand Up @@ -1367,23 +1368,6 @@ def _apply_actions(
cfg[action_dest] = value
return cfg[parent_key] if parent_key else cfg

def merge_config(self, cfg_from: Namespace, cfg_to: Namespace) -> Namespace:
"""Merges the first configuration into the second configuration.

Args:
cfg_from: The configuration from which to merge.
cfg_to: The configuration into which to merge.

Returns:
A new object with the merged configuration.
"""
cfg_from = cfg_from.clone()
cfg_to = cfg_to.clone()
with parser_context(parent_parser=self):
ActionTypeHint.discard_init_args_on_class_path_change(self, cfg_to, cfg_from)
cfg_to.update(cfg_from)
return cfg_to

def _check_value_key(
self, action: argparse.Action, value: Any, key: str, cfg: Namespace | None, append: bool = False
) -> Any:
Expand Down
12 changes: 12 additions & 0 deletions jsonargparse/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ def patched_parse(

return cfg.as_dict() if self._parse_as_dict and not _skip_validation else cfg

patched_parse.__name__ = method_name
patched_parse.__qualname__ = f"ArgumentParser.{method_name}"

setattr(ArgumentParser, unpatched_method_name, getattr(ArgumentParser, method_name))
setattr(ArgumentParser, method_name, patched_parse)

Expand Down Expand Up @@ -750,6 +753,15 @@ def _get_parser_instantiators(self) -> InstantiatorsDictType:
instantiators.update({k: v for k, v in parent_instantiators.items() if k not in instantiators})
return instantiators

@deprecated("""
``ArgumentParser.merge_config`` was deprecated in v4.50.0 and will be
removed in v5.0.0. There is no replacement since this is for internal use.
""")
def merge_config(self, cfg_from: Namespace, cfg_to: Namespace) -> Namespace:
from ._util import merge_config

return merge_config(self, cfg_from, cfg_to)


def deprecated_skip_check(component, kwargs: dict, skip_validation: bool) -> bool:
skip_check = kwargs.pop("skip_check", None)
Expand Down
3 changes: 2 additions & 1 deletion jsonargparse/_subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from ._deprecated import deprecated_implicit_subcommand
from ._namespace import Namespace, NSKeyError, split_key, split_key_root
from ._type_checking import ActionsContainer, ArgumentParser
from ._util import merge_config

__all__ = ["ActionSubCommands"]

Expand Down Expand Up @@ -267,7 +268,7 @@ def handle_subcommands(

# Update all subcommand settings
if subnamespace is not None:
cfg[key] = subparser.merge_config(cfg.get(key, Namespace()), subnamespace)
cfg[key] = merge_config(subparser, cfg.get(key, Namespace()), subnamespace)

# Handle inner subcommands
if subparser._subparsers is not None:
Expand Down
22 changes: 22 additions & 0 deletions jsonargparse/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
parser_context,
)
from ._loaders_dumpers import json_compact_dump, load_value
from ._namespace import Namespace
from ._optionals import _get_config_read_mode
from ._paths import Path
from ._type_checking import ArgumentParser
Expand All @@ -47,6 +48,27 @@ def argument_error(message: str, default_config_file: str | None = None) -> Argu
return ex


def merge_config(parser, source: Namespace, target: Namespace) -> Namespace:
"""Merges the first configuration into the second configuration.

Args:
parser: The parser object.
source: The configuration from which to merge.
target: The configuration into which to merge.

Returns:
A new object with the merged configuration.
"""
from ._typehints import ActionTypeHint

source = source.clone()
target = target.clone()
with parser_context(parent_parser=parser):
ActionTypeHint.discard_init_args_on_class_path_change(parser, target, source)
target.update(source)
return target


def _config_path_id(cfg_path: Path) -> tuple[str, str]:
path_id = cfg_path.absolute
if not (cfg_path.is_url or cfg_path.is_fsspec):
Expand Down
9 changes: 0 additions & 9 deletions jsonargparse_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1158,15 +1158,6 @@ def test_validate_branch(example_parser):
ctx.match("Expected a <class 'int'>")


def test_merge_config(parser):
for key in [1, 2, 3]:
parser.add_argument(f"--op{key}", type=int)
cfg_from = Namespace(op1=1, op2=None)
cfg_to = Namespace(op1=None, op2=2, op3=3)
cfg = parser.merge_config(cfg_from, cfg_to)
assert cfg == Namespace(op1=1, op2=None, op3=3)


def test_strip_unknown(parser, example_parser):
for key, default in example_parser.get_defaults().items():
parser.add_argument("--" + key, default=default)
Expand Down
15 changes: 15 additions & 0 deletions jsonargparse_tests/test_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -1238,3 +1238,18 @@ def test_subcommands_implicit_in_default_config_files(parser, tmp_cwd):
assert cfg.sub == "sub1"
assert cfg.sub1 == Namespace(sub1val=2)
assert "sub2" not in cfg


def test_deprecated_merge_config(parser):
for key in [1, 2, 3]:
parser.add_argument(f"--op{key}", type=int)
cfg_from = Namespace(op1=1, op2=None)
cfg_to = Namespace(op1=None, op2=2, op3=3)
with catch_warnings(record=True) as w:
cfg = parser.merge_config(cfg_from, cfg_to)
assert cfg == Namespace(op1=1, op2=None, op3=3)
assert_deprecation_warn(
w,
message="``ArgumentParser.merge_config`` was deprecated",
code="cfg = parser.merge_config(cfg_from, cfg_to)",
)
13 changes: 12 additions & 1 deletion jsonargparse_tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,23 @@
CaptureParserException,
get_import_path,
import_object,
merge_config,
object_path_serializer,
register_unresolvable_import_paths,
unique,
)
from jsonargparse_tests.conftest import capture_logs


def test_merge_config(parser):
for key in [1, 2, 3]:
parser.add_argument(f"--op{key}", type=int)
cfg_from = Namespace(op1=1, op2=None)
cfg_to = Namespace(op1=None, op2=2, op3=3)
cfg = merge_config(parser, cfg_from, cfg_to)
assert cfg == Namespace(op1=1, op2=None, op3=3)


# logger property tests


Expand Down Expand Up @@ -130,7 +141,7 @@ def test_import_object_invalid():

def test_get_import_path():
assert get_import_path(ArgumentParser) == "jsonargparse.ArgumentParser"
assert get_import_path(ArgumentParser.merge_config) == "jsonargparse.ArgumentParser.merge_config"
assert get_import_path(ArgumentParser.parse_args) == "jsonargparse.ArgumentParser.parse_args"
from email.mime.base import MIMEBase

assert get_import_path(MIMEBase) == "email.mime.base.MIMEBase"
Expand Down
Loading