From 9aee0d7188112afaf5b8fe91fcb114763eec1c08 Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:12:10 +0100 Subject: [PATCH 1/2] dict_to_namespace is deprecated and will be removed in v5.0.0 --- CHANGELOG.rst | 4 ++++ jsonargparse/_deprecated.py | 22 +++++++++++++++++---- jsonargparse/_namespace.py | 28 +++++++++------------------ jsonargparse_tests/test_deprecated.py | 14 ++++++++++++++ jsonargparse_tests/test_namespace.py | 9 +-------- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0e9582ce..f8afced0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -47,6 +47,10 @@ Deprecated - ``compose_dataclasses`` is deprecated and will be removed in v5.0.0. There is no direct replacement, whoever is interested can copy the code (`#796 `__). +- ``dict_to_namespace`` is deprecated and will be removed in v5.0.0. No + replacement is provided because blindly converting a dictionary to a namespace + may not yield the same results as using a parser, which could lead to + confusion. (`#797 `__). v4.42.0 (2025-10-14) diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index 6ded9fe0..db45b642 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -9,11 +9,12 @@ from importlib import import_module from pathlib import Path from types import ModuleType -from typing import Any, Callable, Dict, Optional, Set, overload +from typing import Any, Callable, Dict, Optional, Set, Union, overload from ._common import Action, null_logger from ._common import LoggerProperty as InternalLoggerProperty -from ._namespace import Namespace +from ._namespace import Namespace, recreate_branches +from ._namespace import dict_to_namespace as _dict_to_namespace from ._type_checking import ArgumentParser, ruamelCommentedMap __all__ = [ @@ -28,6 +29,7 @@ "ParserError", "compose_dataclasses", "get_config_read_mode", + "dict_to_namespace", "namespace_to_dict", "null_logger", "set_docstring_parse_options", @@ -123,8 +125,6 @@ def patched_init(self, *args, parse_as_dict: bool = False, **kwargs): ArgumentParser._unpatched_init = ArgumentParser.__init__ ArgumentParser.__init__ = patched_init - from typing import Union - # Patch parse methods def patch_parse_method(method_name): unpatched_method_name = "_unpatched_" + method_name @@ -707,6 +707,20 @@ def namespace_to_dict(namespace: Namespace) -> Dict[str, Any]: return namespace.clone().as_dict() +@deprecated( + """ + dict_to_namespace was deprecated in v4.43.0 and will be removed in v5.0.0. + No replacement is provided because blindly converting a dictionary to a + namespace may not yield the same results as using a parser, which could lead + to confusion. +""" +) +def dict_to_namespace(cfg_dict: dict[str, Any]) -> Namespace: + """Converts a nested dictionary into a nested namespace.""" + cfg_dict = recreate_branches(cfg_dict) + return _dict_to_namespace(cfg_dict) + + @overload def strip_meta(cfg: "Namespace") -> "Namespace": ... # pragma: no cover diff --git a/jsonargparse/_namespace.py b/jsonargparse/_namespace.py index 3498071b..36ed7ecc 100644 --- a/jsonargparse/_namespace.py +++ b/jsonargparse/_namespace.py @@ -15,10 +15,7 @@ Union, ) -__all__ = [ - "Namespace", - "dict_to_namespace", -] +__all__ = ["Namespace"] meta_keys = {"__default_config__", "__path__", "__orig__"} @@ -322,27 +319,20 @@ def del_clash_mark(key: str) -> str: return key -def expand_dict(cfg): - for k, v in cfg.items(): +def expand_dict(data: dict) -> Namespace: + for k, v in data.items(): if isinstance(v, dict) and all(isinstance(k, str) for k in v): - cfg[k] = expand_dict(v) + data[k] = expand_dict(v) elif isinstance(v, list): for nn, vv in enumerate(v): if isinstance(vv, dict) and all(isinstance(k, str) for k in vv): - cfg[k][nn] = expand_dict(vv) - return Namespace(**cfg) + data[k][nn] = expand_dict(vv) + return Namespace(**data) -def dict_to_namespace(cfg_dict: Union[Dict[str, Any], Namespace]) -> Namespace: - """Converts a nested dictionary into a nested namespace. - - Note: Using this function is generally discouraged because it may not - produce the same results as a parser would. However, it remains part - of the public API to support valid use cases and ensure backward - compatibility. - """ - cfg_dict = recreate_branches(cfg_dict) - return expand_dict(cfg_dict) +def dict_to_namespace(data: dict[str, Any]) -> Namespace: + data = recreate_branches(data) + return expand_dict(data) # Temporal to provide backward compatibility in pytorch-lightning diff --git a/jsonargparse_tests/test_deprecated.py b/jsonargparse_tests/test_deprecated.py index 46959942..400a7baf 100644 --- a/jsonargparse_tests/test_deprecated.py +++ b/jsonargparse_tests/test_deprecated.py @@ -33,6 +33,7 @@ LoggerProperty, ParserError, deprecation_warning, + dict_to_namespace, namespace_to_dict, shown_deprecation_warnings, strip_meta, @@ -741,6 +742,19 @@ def test_add_dataclass_arguments(parser, subtests): assert "CustomA title:" in help_str +def test_dict_to_namespace(): + ns1 = Namespace(a=1, b=Namespace(c=2), d=[Namespace(e=3)]) + dic = {"a": 1, "b": {"c": 2}, "d": [{"e": 3}]} + with catch_warnings(record=True) as w: + ns2 = dict_to_namespace(dic) + assert ns1 == ns2 + assert_deprecation_warn( + w, + message="dict_to_namespace was deprecated", + code="ns2 = dict_to_namespace(dic)", + ) + + def test_namespace_to_dict(): ns = Namespace() ns["w"] = 1 diff --git a/jsonargparse_tests/test_namespace.py b/jsonargparse_tests/test_namespace.py index 3ac08afb..4271c93d 100644 --- a/jsonargparse_tests/test_namespace.py +++ b/jsonargparse_tests/test_namespace.py @@ -5,7 +5,7 @@ import pytest -from jsonargparse import Namespace, dict_to_namespace +from jsonargparse import Namespace from jsonargparse._namespace import NSKeyError, meta_keys skip_if_no_setattr_insertion_order = pytest.mark.skipif( @@ -223,13 +223,6 @@ def test_init_invalid(): Namespace(argparse.Namespace(), x=1) -def test_dict_to_namespace(): - ns1 = Namespace(a=1, b=Namespace(c=2), d=[Namespace(e=3)]) - dic = {"a": 1, "b": {"c": 2}, "d": [{"e": 3}]} - ns2 = dict_to_namespace(dic) - assert ns1 == ns2 - - def test_use_for_kwargs(): def func(a=1, b=2, c=3): return a, b, c From de701e5d93c826778b6799d47b0b388632037c1e Mon Sep 17 00:00:00 2001 From: Mauricio Villegas <5780272+mauvilsa@users.noreply.github.com> Date: Thu, 30 Oct 2025 09:37:13 +0100 Subject: [PATCH 2/2] Small fix --- jsonargparse/_deprecated.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index db45b642..f2e62627 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -13,8 +13,7 @@ from ._common import Action, null_logger from ._common import LoggerProperty as InternalLoggerProperty -from ._namespace import Namespace, recreate_branches -from ._namespace import dict_to_namespace as _dict_to_namespace +from ._namespace import Namespace from ._type_checking import ArgumentParser, ruamelCommentedMap __all__ = [ @@ -717,7 +716,8 @@ def namespace_to_dict(namespace: Namespace) -> Dict[str, Any]: ) def dict_to_namespace(cfg_dict: dict[str, Any]) -> Namespace: """Converts a nested dictionary into a nested namespace.""" - cfg_dict = recreate_branches(cfg_dict) + from ._namespace import dict_to_namespace as _dict_to_namespace + return _dict_to_namespace(cfg_dict)