Skip to content

Commit 8ad02de

Browse files
authored
List of paths types improvements (#816)
1 parent 966d78d commit 8ad02de

File tree

5 files changed

+60
-8
lines changed

5 files changed

+60
-8
lines changed

CHANGELOG.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@ The semantic versioning only considers the public API as described in
1212
paths are considered internals and can change in minor and patch releases.
1313

1414

15-
v4.44.1 (unreleased)
15+
v4.45.0 (unreleased)
1616
--------------------
1717

18+
Added
19+
^^^^^
20+
- Signature methods now when given ``sub_configs=True``, list of paths types can
21+
now receive a file containing a list of paths (`#816
22+
<https://github.com/omni-us/jsonargparse/pull/816>`__).
23+
1824
Fixed
1925
^^^^^
2026
- Evaluation of postponed annotations for dataclass inheritance across modules
@@ -23,6 +29,12 @@ Fixed
2329
- Getting parameter descriptions from docstrings not working for dataclass
2430
inheritance (`#815 <https://github.com/omni-us/jsonargparse/pull/815>`__).
2531

32+
Changed
33+
^^^^^^^
34+
- List of paths types now show in the help the supported options for providing
35+
paths like ``'["PATH1",...]' | LIST_OF_PATHS_FILE | -`` (`#816
36+
<https://github.com/omni-us/jsonargparse/pull/816>`__).
37+
2638

2739
v4.44.0 (2025-11-25)
2840
--------------------

jsonargparse/_signatures.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
LazyInitBaseClass,
2525
callable_instances,
2626
get_subclass_names,
27+
is_list_pathlike,
2728
is_optional,
2829
not_required_types,
2930
sequence_origin_types,
@@ -419,7 +420,9 @@ def _add_signature_parameter(
419420
else:
420421
register_pydantic_type(annotation)
421422
enable_path = sub_configs and (
422-
is_subclass_typehint or ActionTypeHint.is_return_subclass_typehint(annotation)
423+
is_subclass_typehint
424+
or ActionTypeHint.is_return_subclass_typehint(annotation)
425+
or is_list_pathlike(annotation)
423426
)
424427
args = ActionTypeHint.prepare_add_argument(
425428
args=args,

jsonargparse/_typehints.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ def _check_type(self, value, append=False, cfg=None, mode=None):
592592
):
593593
ex = None
594594
elif self._enable_path and config_path is None and isinstance(orig_val, str):
595-
msg = f"\n- Expected a config path but {orig_val} either not accessible or invalid\n- "
595+
msg = f"\n- Expected a path but {orig_val} either not accessible or invalid\n- "
596596
raise type(ex)(msg + str(ex)) from ex
597597
if ex:
598598
raise ex
@@ -713,6 +713,14 @@ def is_pathlike(typehint) -> bool:
713713
return is_subclass(typehint, os.PathLike)
714714

715715

716+
def is_list_pathlike(typehint) -> bool:
717+
typehint_origin = get_typehint_origin(typehint)
718+
if typehint_origin in sequence_origin_types:
719+
subtype = typehint.__args__[0]
720+
return is_pathlike(subtype)
721+
return False
722+
723+
716724
def raise_unexpected_value(message: str, val: Any = inspect._empty, exception: Optional[Exception] = None) -> NoReturn:
717725
if val is not inspect._empty:
718726
message += f". Got value: {val}"
@@ -1643,7 +1651,9 @@ def typehint_metavar(typehint):
16431651
elif is_optional(typehint, Enum):
16441652
enum = typehint.__args__[0]
16451653
metavar = iter_to_set_str(list(enum.__members__) + ["null"])
1646-
elif typehint_origin in tuple_set_origin_types:
1654+
elif is_list_pathlike(typehint):
1655+
metavar = "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -"
1656+
elif typehint_origin in tuple_set_origin_types or typehint_origin in sequence_origin_types:
16471657
metavar = "[ITEM,...]"
16481658
return metavar
16491659

jsonargparse_tests/test_dataclasses.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ def __init__(self, prm_1: float, prm_2: bool):
626626
],
627627
)
628628
def test_class_path_union_mixture_dataclass_and_class(parser, union_type):
629-
parser.add_argument("--union", type=union_type, enable_path=True)
629+
parser.add_argument("--union", type=union_type)
630630

631631
value = {"class_path": f"{__name__}.UnionData", "init_args": {"data_a": 2, "data_b": "x"}}
632632
cfg = parser.parse_args([f"--union={json.dumps(value)}"])

jsonargparse_tests/test_paths.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ def test_enable_path_dict(parser, tmp_cwd):
537537
assert data == cfg["data"]
538538
with pytest.raises(ArgumentError) as ctx:
539539
parser.parse_args(["--data=does-not-exist.yaml"])
540-
ctx.match("does-not-exist.yaml either not accessible or invalid")
540+
ctx.match("Expected a path but does-not-exist.yaml either not accessible or invalid")
541541

542542

543543
def test_enable_path_subclass(parser, tmp_cwd):
@@ -616,8 +616,10 @@ def test_enable_path_list_path_fr(parser, tmp_cwd, mock_stdin, subtests):
616616
with subtests.test("paths list nargs='+' path not exist"):
617617
pytest.raises(ArgumentError, lambda: parser.parse_args(["--lists", str(list_file4)]))
618618

619-
with subtests.test("paths list nargs='+' list not exist"):
620-
pytest.raises(ArgumentError, lambda: parser.parse_args(["--lists", "no-such-file"]))
619+
with subtests.test("paths list nargs='+' list not exist"): # TODO: check error message
620+
with pytest.raises(ArgumentError) as ctx:
621+
parser.parse_args(["--lists", "no-such-file"])
622+
ctx.match("Expected a path but no-such-file either not accessible or invalid")
621623

622624

623625
def test_enable_path_list_path_fr_default_stdin(parser, tmp_cwd, mock_stdin, subtests):
@@ -643,6 +645,31 @@ def test_enable_path_list_path_fr_default_stdin(parser, tmp_cwd, mock_stdin, sub
643645
assert all(isinstance(x, Path_fr) for x in cfg.list)
644646
assert ["file1", "file2"] == [str(x) for x in cfg.list]
645647

648+
with subtests.test("help"):
649+
help_str = get_parser_help(parser)
650+
assert "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -" in help_str
651+
652+
653+
class ClassListPath:
654+
def __init__(self, files: list[Path_fr]):
655+
self.files = files
656+
657+
658+
def test_add_class_list_path(parser, tmp_cwd):
659+
(tmp_cwd / "file1").touch()
660+
(tmp_cwd / "file2").touch()
661+
list_file1 = tmp_cwd / "files.lst"
662+
list_file1.write_text("file1\nfile2\n")
663+
664+
parser.add_class_arguments(ClassListPath, "cls", sub_configs=True)
665+
666+
cfg = parser.parse_args([f"--cls.files={list_file1}"])
667+
assert all(isinstance(x, Path_fr) for x in cfg.cls.files)
668+
assert ["file1", "file2"] == [str(x) for x in cfg.cls.files]
669+
670+
help_str = get_parser_help(parser)
671+
assert "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -" in help_str
672+
646673

647674
class DataOptionalPath:
648675
def __init__(self, path: Optional[os.PathLike] = None):

0 commit comments

Comments
 (0)