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
14 changes: 13 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ The semantic versioning only considers the public API as described in
paths are considered internals and can change in minor and patch releases.


v4.44.1 (unreleased)
v4.45.0 (unreleased)
--------------------

Added
^^^^^
- Signature methods now when given ``sub_configs=True``, list of paths types can
now receive a file containing a list of paths (`#816
<https://github.com/omni-us/jsonargparse/pull/816>`__).

Fixed
^^^^^
- Evaluation of postponed annotations for dataclass inheritance across modules
Expand All @@ -23,6 +29,12 @@ Fixed
- Getting parameter descriptions from docstrings not working for dataclass
inheritance (`#815 <https://github.com/omni-us/jsonargparse/pull/815>`__).

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


v4.44.0 (2025-11-25)
--------------------
Expand Down
5 changes: 4 additions & 1 deletion jsonargparse/_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
LazyInitBaseClass,
callable_instances,
get_subclass_names,
is_list_pathlike,
is_optional,
not_required_types,
sequence_origin_types,
Expand Down Expand Up @@ -419,7 +420,9 @@ def _add_signature_parameter(
else:
register_pydantic_type(annotation)
enable_path = sub_configs and (
is_subclass_typehint or ActionTypeHint.is_return_subclass_typehint(annotation)
is_subclass_typehint
or ActionTypeHint.is_return_subclass_typehint(annotation)
or is_list_pathlike(annotation)
)
args = ActionTypeHint.prepare_add_argument(
args=args,
Expand Down
14 changes: 12 additions & 2 deletions jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ def _check_type(self, value, append=False, cfg=None, mode=None):
):
ex = None
elif self._enable_path and config_path is None and isinstance(orig_val, str):
msg = f"\n- Expected a config path but {orig_val} either not accessible or invalid\n- "
msg = f"\n- Expected a path but {orig_val} either not accessible or invalid\n- "
raise type(ex)(msg + str(ex)) from ex
if ex:
raise ex
Expand Down Expand Up @@ -713,6 +713,14 @@ def is_pathlike(typehint) -> bool:
return is_subclass(typehint, os.PathLike)


def is_list_pathlike(typehint) -> bool:
typehint_origin = get_typehint_origin(typehint)
if typehint_origin in sequence_origin_types:
subtype = typehint.__args__[0]
return is_pathlike(subtype)
return False


def raise_unexpected_value(message: str, val: Any = inspect._empty, exception: Optional[Exception] = None) -> NoReturn:
if val is not inspect._empty:
message += f". Got value: {val}"
Expand Down Expand Up @@ -1643,7 +1651,9 @@ def typehint_metavar(typehint):
elif is_optional(typehint, Enum):
enum = typehint.__args__[0]
metavar = iter_to_set_str(list(enum.__members__) + ["null"])
elif typehint_origin in tuple_set_origin_types:
elif is_list_pathlike(typehint):
metavar = "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -"
elif typehint_origin in tuple_set_origin_types or typehint_origin in sequence_origin_types:
metavar = "[ITEM,...]"
return metavar

Expand Down
2 changes: 1 addition & 1 deletion jsonargparse_tests/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ def __init__(self, prm_1: float, prm_2: bool):
],
)
def test_class_path_union_mixture_dataclass_and_class(parser, union_type):
parser.add_argument("--union", type=union_type, enable_path=True)
parser.add_argument("--union", type=union_type)

value = {"class_path": f"{__name__}.UnionData", "init_args": {"data_a": 2, "data_b": "x"}}
cfg = parser.parse_args([f"--union={json.dumps(value)}"])
Expand Down
33 changes: 30 additions & 3 deletions jsonargparse_tests/test_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@
assert data == cfg["data"]
with pytest.raises(ArgumentError) as ctx:
parser.parse_args(["--data=does-not-exist.yaml"])
ctx.match("does-not-exist.yaml either not accessible or invalid")
ctx.match("Expected a path but does-not-exist.yaml either not accessible or invalid")


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

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


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

with subtests.test("help"):
help_str = get_parser_help(parser)
assert "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -" in help_str


class ClassListPath:
def __init__(self, files: list[Path_fr]):
self.files = files


def test_add_class_list_path(parser, tmp_cwd):
(tmp_cwd / "file1").touch()
(tmp_cwd / "file2").touch()
list_file1 = tmp_cwd / "files.lst"
list_file1.write_text("file1\nfile2\n")

parser.add_class_arguments(ClassListPath, "cls", sub_configs=True)

cfg = parser.parse_args([f"--cls.files={list_file1}"])
assert all(isinstance(x, Path_fr) for x in cfg.cls.files)
assert ["file1", "file2"] == [str(x) for x in cfg.cls.files]

help_str = get_parser_help(parser)
assert "'[\"PATH1\",...]' | LIST_OF_PATHS_FILE | -" in help_str


class DataOptionalPath:
def __init__(self, path: Optional[os.PathLike] = None):
Expand Down
Loading