Skip to content

Commit d22177c

Browse files
committed
Accept broken symlinks as path arguments of dandi validate
`dandi validate` already handles annexed files whose content has not been fetched (the broken symlinks of a DataLad dataset) according to `--missing-file-content`: `error`, `skip`, or `only-non-data`. Those policies were applied only when such a file was reached through its directory, though: given directly on the command line, the file never got past argument parsing, because `click.Path(exists=True)` follows the link and rejected it with "does not exist". Check the path with `lexists()` instead, so that dandi validate --missing-file-content=skip sub-01/sub-01.nwb works the same as validating `sub-01/`, while paths that truly do not exist are still rejected with the same error. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFhMPs6vF5zyeHDvPTvRcA
1 parent c3931cc commit d22177c

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎dandi/cli/cmd_validate.py‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
import re
99
import sys
10-
from typing import IO, Union, cast
10+
from typing import IO, Any, Union, cast
1111
import warnings
1212

1313
import click
@@ -35,6 +35,28 @@ class TruncationNotice:
3535
"""Number of validation results omitted from this group."""
3636

3737

38+
class ExistingPath(click.Path):
39+
"""
40+
Like ``click.Path(exists=True)``, but also accepting broken symbolic links,
41+
such as the annexed files of a DataLad dataset whose content has not been
42+
fetched. ``click.Path(exists=True)`` follows the link and rejects those as
43+
nonexistent, which prevented the ``--missing-file-content`` policies
44+
(``error``, ``skip``, ``only-non-data``) from ever being applied to a file
45+
given directly on the command line rather than via its directory.
46+
"""
47+
48+
def convert(
49+
self, value: Any, param: click.Parameter | None, ctx: click.Context | None
50+
) -> Any:
51+
if not os.path.lexists(value):
52+
self.fail(
53+
f"{self.name.title()} {os.fsdecode(value)!r} does not exist.",
54+
param,
55+
ctx,
56+
)
57+
return super().convert(value, param, ctx)
58+
59+
3860
STRUCTURED_FORMATS = ("json", "json_pp", "json_lines", "yaml")
3961

4062
_EXT_TO_FORMAT = {
@@ -228,7 +250,7 @@ def validate_bids(
228250
multiple=True,
229251
default=(),
230252
)
231-
@click.argument("paths", nargs=-1, type=click.Path(exists=True, dir_okay=True))
253+
@click.argument("paths", nargs=-1, type=ExistingPath(dir_okay=True))
232254
@click.pass_context
233255
@devel_debug_option()
234256
@map_to_click_exceptions

‎dandi/cli/tests/test_cmd_validate.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,3 +996,25 @@ def test_validate_missing_file_content_no_broken_symlinks(tmp_path: Path) -> Non
996996
assert r_default.exit_code == r_skip.exit_code == 0
997997
assert "FILE_CONTENT_MISSING" not in r_default.output
998998
assert "FILE_CONTENT_MISSING" not in r_skip.output
999+
1000+
1001+
@pytest.mark.ai_generated
1002+
def test_validate_broken_symlink_path_argument(tmp_path: Path) -> None:
1003+
"""A broken symlink can be given directly as a path to validate.
1004+
1005+
The --missing-file-content policies then apply to it just as they do when
1006+
the containing directory is given.
1007+
"""
1008+
ds = _make_dandiset_with_broken_symlinks(tmp_path)
1009+
link = ds / "sub-001" / "sub-001.nwb"
1010+
r = CliRunner().invoke(validate, [str(link)])
1011+
assert r.exit_code == 1
1012+
assert "does not exist" not in r.output
1013+
assert "FILE_CONTENT_MISSING" in r.output
1014+
r = CliRunner().invoke(validate, ["--missing-file-content", "skip", str(link)])
1015+
assert r.exit_code == 0
1016+
assert "FILE_CONTENT_MISSING_SKIPPED" in r.output
1017+
# Paths that truly do not exist are still rejected
1018+
r = CliRunner().invoke(validate, [str(ds / "sub-001" / "nonexistent.nwb")])
1019+
assert r.exit_code == 2
1020+
assert "does not exist" in r.output

0 commit comments

Comments
 (0)