Skip to content

Commit ceb7202

Browse files
authored
Fix misleading error message when a namespace is used in a list comprehension and diverse refactorings (#772)
1 parent df20921 commit ceb7202

File tree

10 files changed

+44
-32
lines changed

10 files changed

+44
-32
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ repos:
8686
if [ "${BUMPVERSION_NEW_VERSION+x}" = "" ]; then
8787
echo "$(tput setaf 6) Skipped, only runs when bumping version $(tput sgr0)";
8888
else
89-
CHANGELOG=$(grep -E "^v.+\..+\..+ \(....-..-..\)" CHANGELOG.rst | head -n 1);
89+
CHANGELOG=$(grep -E "^v.+\..+\..+ \(.*\)" CHANGELOG.rst | head -n 1);
9090
EXPECTED="v$BUMPVERSION_NEW_VERSION ($(date -u +%Y-%m-%d))";
9191
if [ "$CHANGELOG" != "$EXPECTED" ] && [ $(echo $BUMPVERSION_NEW_VERSION | grep -cE "[0-9.]+(\.dev|rc)[0-9]+") = 0 ]; then
9292
if [ $(grep -c "^v$BUMPVERSION_NEW_VERSION " CHANGELOG.rst) = 1 ]; then

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Fixed
2727
<https://github.com/omni-us/jsonargparse/pull/770>`__).
2828
- ``dataclass`` with default failing when ``validate_defaults=True`` (`#771
2929
<https://github.com/omni-us/jsonargparse/pull/771>`__).
30+
- Misleading error message when a namespace is used in a list comprehension
31+
(`#772 <https://github.com/omni-us/jsonargparse/pull/772>`__).
3032

3133

3234
v4.41.0 (2025-09-04)

CONTRIBUTING.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,17 +154,16 @@ example:
154154

155155
.. code-block::
156156
157-
v4.28.0 (2024-03-??)
157+
v4.28.0 (unreleased)
158158
--------------------
159159
160160
Added
161161
^^^^^
162162
-
163163
164-
If no such section exists, just add it. New sections should include ``-??`` in
165-
the date to illustrate that the release date is not known yet. Have a look at
166-
previous releases to decide under which subsection the new entry should go. If
167-
you are unsure, ask in the pull request.
164+
If no such section exists, just add it with "(unreleased)" instead of a date.
165+
Have a look at previous releases to decide under which subsection the new entry
166+
should go. If you are unsure, ask in the pull request.
168167

169168
Please don't open pull requests with breaking changes unless this has been
170169
discussed and agreed upon in an issue.

jsonargparse/_namespace.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ def _parse_key(self, key: str) -> Tuple[str, Optional["Namespace"], str]:
126126
Raises:
127127
KeyError: When given invalid key.
128128
"""
129+
if not isinstance(key, str):
130+
raise NSKeyError(f"Key must be a string, got: {key!r}.")
129131
if " " in key:
130132
raise NSKeyError(f'Spaces not allowed in keys: "{key}".')
131133
key_split = split_key(key)
@@ -292,9 +294,9 @@ def update(
292294
self[key] = value
293295
else:
294296
prefix = key + "." if key else ""
295-
for key, val in value.items():
296-
if not only_unset or prefix + key not in self:
297-
self[prefix + key] = val
297+
for subkey, subval in value.items():
298+
if not only_unset or prefix + subkey not in self:
299+
self[prefix + subkey] = subval
298300
return self
299301

300302
def get(self, key: str, default: Any = None) -> Any:

jsonargparse/_parameter_resolvers.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -710,19 +710,19 @@ def replace_param_default_subclass_specs(self, params: List[ParamData]) -> None:
710710
subclass_types = get_subclass_types(param.annotation, callable_return=True)
711711
if not (class_type and subclass_types and is_subclass(class_type, subclass_types)):
712712
continue
713-
subclass_spec: dict = {"class_path": get_import_path(class_type), "init_args": {}}
713+
default: dict = {"class_path": get_import_path(class_type), "init_args": {}}
714714
for kwarg in node.keywords:
715715
if kwarg.arg and ast_is_constant(kwarg.value):
716-
subclass_spec["init_args"][kwarg.arg] = ast_get_constant_value(kwarg.value)
716+
default["init_args"][kwarg.arg] = ast_get_constant_value(kwarg.value)
717717
else:
718-
subclass_spec.clear()
718+
default.clear()
719719
break
720-
if not subclass_spec or len(node.args) - num_positionals > 0:
720+
if not default or len(node.args) - num_positionals > 0:
721721
self.log_debug(f"unsupported class instance default: {ast_str(default_node)}")
722-
elif subclass_spec:
723-
if not subclass_spec["init_args"]:
724-
del subclass_spec["init_args"]
725-
param.default = subclass_spec
722+
elif default:
723+
if not default["init_args"]:
724+
del default["init_args"]
725+
param.default = default
726726

727727
def get_call_class_type(self, node) -> Optional[type]:
728728
names = ast_get_name_and_attrs(getattr(node, "func", None))

jsonargparse_tests/test_dataclasses.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,16 @@ def test_class_path_union_mixture_dataclass_and_class(parser, union_type):
646646
class DataClassWithAliasType:
647647
p1: IntOrString # type: ignore[valid-type]
648648

649-
def test_bare_alias_type(parser):
649+
if annotated:
650+
651+
@dataclasses.dataclass
652+
class DataClassWithAnnotatedAliasType:
653+
p1: annotated[IntOrString, 1] # type: ignore[valid-type]
654+
655+
656+
@pytest.mark.skipif(not type_alias_type, reason="TypeAliasType is required")
657+
class TestTypeAliasType:
658+
def test_bare_alias_type(self, parser):
650659
parser.add_argument("--data", type=IntOrString)
651660
help_str = get_parser_help(parser)
652661
help_str_lines = [line for line in help_str.split("\n") if "type: IntOrString" in line]
@@ -657,7 +666,7 @@ def test_bare_alias_type(parser):
657666
cfg = parser.parse_args(["--data=3"])
658667
assert cfg.data == 3
659668

660-
def test_dataclass_with_alias_type(parser):
669+
def test_dataclass_with_alias_type(self, parser):
661670
parser.add_argument("--data", type=DataClassWithAliasType)
662671
help_str = get_parser_help(parser)
663672
help_str_lines = [line for line in help_str.split("\n") if "type: IntOrString" in line]
@@ -669,7 +678,7 @@ def test_dataclass_with_alias_type(parser):
669678
assert cfg.data.p1 == 3
670679

671680
@pytest.mark.skipif(not annotated, reason="Annotated is required")
672-
def test_annotated_alias_type(parser):
681+
def test_annotated_alias_type(self, parser):
673682
parser.add_argument("--data", type=annotated[IntOrString, 1])
674683
help_str = get_parser_help(parser)
675684
help_str_lines = [line for line in help_str.split("\n") if "type: Annotated[IntOrString, 1]" in line]
@@ -680,14 +689,8 @@ def test_annotated_alias_type(parser):
680689
cfg = parser.parse_args(["--data=3"])
681690
assert cfg.data == 3
682691

683-
if annotated:
684-
685-
@dataclasses.dataclass
686-
class DataClassWithAnnotatedAliasType:
687-
p1: annotated[IntOrString, 1] # type: ignore[valid-type]
688-
689692
@pytest.mark.skipif(not annotated, reason="Annotated is required")
690-
def test_dataclass_with_annotated_alias_type(parser):
693+
def test_dataclass_with_annotated_alias_type(self, parser):
691694
parser.add_argument("--data", type=DataClassWithAnnotatedAliasType)
692695
help_str = get_parser_help(parser)
693696
# The printable field datatype is not uniform across versions.

jsonargparse_tests/test_namespace.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77

88
from jsonargparse import Namespace, dict_to_namespace
9-
from jsonargparse._namespace import meta_keys
9+
from jsonargparse._namespace import NSKeyError, meta_keys
1010

1111
skip_if_no_setattr_insertion_order = pytest.mark.skipif(
1212
platform.python_implementation() != "CPython",
@@ -151,6 +151,12 @@ def test_values_generator():
151151
assert values == [1, 2, 3, {"x": 4, "y": 5}]
152152

153153

154+
def test_non_str_keys():
155+
ns = Namespace(a=Namespace(b=Namespace(c=1)))
156+
with pytest.raises(NSKeyError, match="Key must be a string, got: 0"):
157+
[x for x in ns.a.b]
158+
159+
154160
def test_namespace_from_dict():
155161
dic = {"a": 1, "b": {"c": 2}}
156162
ns = Namespace(dic)

jsonargparse_tests/test_pydantic.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,8 @@ def test_pydantic_types(self, valid_value, invalid_value, cast, type_str, monkey
249249
assert cast(cfg.model.param) == valid_value
250250
dump = json_or_yaml_load(parser.dump(cfg))
251251
assert dump == {"model": {"param": valid_value}}
252-
with pytest.raises(ArgumentError) as ctx:
252+
with pytest.raises(ArgumentError, match='Parser key "model.param"'):
253253
parser.parse_args([f"--model.param={invalid_value}"])
254-
ctx.match("model.param")
255254

256255
@pytest.mark.skipif(not pydantic_supports_field_init, reason="Field.init is required")
257256
def test_dataclass_field_init_false(self, parser):

jsonargparse_tests/test_stubs_resolver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ def test_get_params_complex_function_requests_get(parser):
291291
assert ["url", "params"] == list(parser.get_defaults().keys())
292292
help_str = get_parser_help(parser)
293293
assert "default: Unknown<stubs-resolver>" in help_str
294+
assert "--cookies.help CLASS_PATH_OR_NAME" in help_str
294295

295296

296297
# stubs only resolver tests

jsonargparse_tests/test_typehints.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ def test_valid_unpack_typeddict(parser, init_args):
720720
if test_config["test"]["init_args"].get("b") is None:
721721
# parser.dump does not dump null b
722722
test_config["test"]["init_args"].pop("b", None)
723-
assert json.dumps({"testclass": test_config}).replace(" ", "") == parser.dump(cfg, format="json")
723+
assert test_config == json.loads(parser.dump(cfg, format="json"))["testclass"]
724724

725725

726726
@pytest.mark.skipif(not Unpack, reason="Unpack introduced in python 3.11 or backported in typing_extensions")
@@ -743,7 +743,7 @@ def test_valid_inherited_unpack_typeddict(parser, init_args):
743743
if test_config["init_args"].get("b") is None:
744744
# parser.dump does not dump null b
745745
test_config["init_args"].pop("b", None)
746-
assert json.dumps({"testclass": test_config}).replace(" ", "") == parser.dump(cfg, format="json")
746+
assert test_config == json.loads(parser.dump(cfg, format="json"))["testclass"]
747747

748748

749749
@pytest.mark.skipif(not Unpack, reason="Unpack introduced in python 3.11 or backported in typing_extensions")

0 commit comments

Comments
 (0)