From 2f53b909020c5cd5c11a5029e023e385d4843af6 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Fri, 26 Jun 2026 09:56:52 +0200 Subject: [PATCH] Deprecate ArgumentParser.merge_config since it is considered internal --- CHANGELOG.rst | 3 +++ jsonargparse/_actions.py | 7 +++--- jsonargparse/_completions.py | 4 ++-- jsonargparse/_core.py | 34 +++++++-------------------- jsonargparse/_deprecated.py | 12 ++++++++++ jsonargparse/_subcommands.py | 3 ++- jsonargparse/_util.py | 22 +++++++++++++++++ jsonargparse_tests/test_core.py | 9 ------- jsonargparse_tests/test_deprecated.py | 15 ++++++++++++ jsonargparse_tests/test_util.py | 13 +++++++++- 10 files changed, 81 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dd526ded..1512b2a3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 `__). +- ``ArgumentParser.merge_config`` is deprecated and will be removed in v5.0.0. + There is no replacement since it is considered internal (`#925 + `__). v4.49.0 (2026-05-15) diff --git a/jsonargparse/_actions.py b/jsonargparse/_actions.py index a83f22c7..77bd9262 100644 --- a/jsonargparse/_actions.py +++ b/jsonargparse/_actions.py @@ -23,6 +23,7 @@ indent_text, iter_to_set_str, load_config_path_context, + merge_config, parse_value_or_config, ) @@ -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] = [] @@ -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 diff --git a/jsonargparse/_completions.py b/jsonargparse/_completions.py index fef1ee17..f618962e 100644 --- a/jsonargparse/_completions.py +++ b/jsonargparse/_completions.py @@ -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): @@ -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(): diff --git a/jsonargparse/_core.py b/jsonargparse/_core.py index f4c5335d..c3159591 100644 --- a/jsonargparse/_core.py +++ b/jsonargparse/_core.py @@ -94,6 +94,7 @@ get_private_kwargs, identity, load_config_path_context, + merge_config, return_parser_if_captured, ) @@ -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 @@ -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) @@ -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, @@ -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, @@ -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( @@ -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: diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index a024c009..818461e4 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -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) @@ -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) diff --git a/jsonargparse/_subcommands.py b/jsonargparse/_subcommands.py index 37587b2b..13a77a7e 100644 --- a/jsonargparse/_subcommands.py +++ b/jsonargparse/_subcommands.py @@ -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"] @@ -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: diff --git a/jsonargparse/_util.py b/jsonargparse/_util.py index 62617b94..51ede66f 100644 --- a/jsonargparse/_util.py +++ b/jsonargparse/_util.py @@ -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 @@ -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): diff --git a/jsonargparse_tests/test_core.py b/jsonargparse_tests/test_core.py index 17a994f9..ce7c61ca 100644 --- a/jsonargparse_tests/test_core.py +++ b/jsonargparse_tests/test_core.py @@ -1158,15 +1158,6 @@ def test_validate_branch(example_parser): ctx.match("Expected a ") -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) diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py index 6f4830e7..1030f031 100644 --- a/jsonargparse_tests/test_deprecated.py +++ b/jsonargparse_tests/test_deprecated.py @@ -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)", + ) diff --git a/jsonargparse_tests/test_util.py b/jsonargparse_tests/test_util.py index 30a9c1cb..62eb9bdf 100644 --- a/jsonargparse_tests/test_util.py +++ b/jsonargparse_tests/test_util.py @@ -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 @@ -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"