Skip to content

Commit 26cec27

Browse files
authored
Merge branch 'master' into fix/1242-age-reference
2 parents af6490f + eb37fdb commit 26cec27

11 files changed

Lines changed: 141 additions & 9 deletions

File tree

‎.autorc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[
1010
"exec",
1111
{
12-
"afterRelease": "python -m build && twine upload dist/*"
12+
"afterPublish": "python -m build && twine upload dist/*"
1313
}
1414
],
1515
"released"

‎.github/workflows/docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
fetch-depth: 0
2424

2525
- name: Set up Python ${{ matrix.python }}
26-
uses: actions/setup-python@v6
26+
uses: actions/setup-python@v7
2727
with:
2828
python-version: ${{ matrix.python }}
2929

‎.github/workflows/lint.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
with:
1414
fetch-depth: 0
1515
- name: Set up Python
16-
uses: actions/setup-python@v6
16+
uses: actions/setup-python@v7
1717
with:
1818
python-version: '3.11'
1919
- name: Install dependencies

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
chmod a+x ~/auto
2525
2626
- name: Set up Python
27-
uses: actions/setup-python@v6
27+
uses: actions/setup-python@v7
2828
with:
2929
python-version: '3.11'
3030

‎.github/workflows/run-tests.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ jobs:
8383
fetch-depth: 0
8484

8585
- name: Set up Python ${{ matrix.python }}
86-
uses: actions/setup-python@v6
86+
uses: actions/setup-python@v7
8787
with:
8888
python-version: ${{ matrix.python }}
8989

‎.github/workflows/typing.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fetch-depth: 0
1515

1616
- name: Set up Python
17-
uses: actions/setup-python@v6
17+
uses: actions/setup-python@v7
1818
with:
1919
python-version: '3.11'
2020

‎CHANGELOG.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
# 0.81.0 (Wed Sep 30 2026)
2+
3+
#### 🚀 Enhancement
4+
5+
- Remove deprecated validate-bids command [#1943](https://github.com/dandi/dandi-cli/pull/1943) ([@claude](https://github.com/claude) [@yarikoptic-gitmate](https://github.com/yarikoptic-gitmate))
6+
7+
#### 🐛 Bug Fix
8+
9+
- Accept broken symlinks as path arguments of `dandi validate` [#1937](https://github.com/dandi/dandi-cli/pull/1937) ([@claude](https://github.com/claude) [@CodyCBakerPhD](https://github.com/CodyCBakerPhD))
10+
11+
#### 🏠 Internal
12+
13+
- Run build & twine upload in afterPublish instead of afterRelease [#1945](https://github.com/dandi/dandi-cli/pull/1945) ([@claude](https://github.com/claude) [@yarikoptic-gitmate](https://github.com/yarikoptic-gitmate))
14+
15+
#### 📝 Documentation
16+
17+
- Update docs: bring the command-line reference in sync with the CLI [#1934](https://github.com/dandi/dandi-cli/pull/1934) ([@claude](https://github.com/claude) [@CodyCBakerPhD](https://github.com/CodyCBakerPhD))
18+
19+
#### Authors: 3
20+
21+
- Claude ([@claude](https://github.com/claude))
22+
- Cody Baker ([@CodyCBakerPhD](https://github.com/CodyCBakerPhD))
23+
- GitMate for @yarikoptic ([@yarikoptic-gitmate](https://github.com/yarikoptic-gitmate))
24+
25+
---
26+
127
# 0.80.1 (Sat Sep 26 2026)
228

329
#### 🐛 Bug Fix

‎dandi/cli/base.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from enum import Enum
22
from functools import wraps
33
import os
4+
from typing import Any
45

56
import click
7+
from click.utils import format_filename
68

79
from .. import get_logger
810

@@ -70,6 +72,44 @@ def get_metavar(self, param, ctx=None):
7072
return "[" + ",".join(self.values) + ",all]"
7173

7274

75+
class LinkAwarePath(click.Path):
76+
"""
77+
A ``click.Path`` for commands that should accept broken symlinks
78+
79+
The typical broken symlink is a git-annex (or DataLad) file whose content
80+
has not been fetched. A command like ``dandi validate`` should still take
81+
such a path, to report on it or skip it, rather than refuse it as missing.
82+
"""
83+
84+
def __init__(self, *, lexists: bool = False, **kwargs: Any) -> None:
85+
"""
86+
Parameters
87+
----------
88+
lexists
89+
If True, the path must exist, but a symlink to a missing target
90+
counts as existing (unlike with ``exists=True``). Cannot be
91+
combined with ``resolve_path=True``.
92+
**kwargs
93+
Passed to ``click.Path``.
94+
"""
95+
if lexists and kwargs.get("resolve_path"):
96+
raise ValueError("lexists=True cannot be combined with resolve_path=True")
97+
super().__init__(**kwargs)
98+
self.lexists = lexists
99+
100+
def convert(
101+
self, value: Any, param: click.Parameter | None, ctx: click.Context | None
102+
) -> Any:
103+
is_dash = self.file_okay and self.allow_dash and value in ("-", b"-")
104+
if self.lexists and not is_dash and not os.path.lexists(value):
105+
self.fail(
106+
f"{self.name.title()} {format_filename(value)!r} does not exist.",
107+
param,
108+
ctx,
109+
)
110+
return super().convert(value, param, ctx)
111+
112+
73113
# ???: could make them always available but hidden
74114
# via hidden=True.
75115
def devel_option(*args, **kwargs):

‎dandi/cli/cmd_validate.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111

1212
import click
1313

14-
from .base import devel_debug_option, devel_option, map_to_click_exceptions
14+
from .base import (
15+
LinkAwarePath,
16+
devel_debug_option,
17+
devel_option,
18+
map_to_click_exceptions,
19+
)
1520
from .formatter import JSONFormatter, JSONLinesFormatter, TextFormatter, YAMLFormatter
1621
from ..utils import pluralize
1722
from ..validate._core import validate as validate_
@@ -176,7 +181,7 @@ def _filter_results(
176181
multiple=True,
177182
default=(),
178183
)
179-
@click.argument("paths", nargs=-1, type=click.Path(exists=True, dir_okay=True))
184+
@click.argument("paths", nargs=-1, type=LinkAwarePath(lexists=True, dir_okay=True))
180185
@click.pass_context
181186
@devel_debug_option()
182187
@map_to_click_exceptions

‎dandi/cli/tests/test_base.py‎

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from enum import StrEnum
2+
import os
3+
from pathlib import Path
24

35
import click
46
from click.testing import CliRunner
57
import pytest
68

7-
from ..base import EnumChoice
9+
from ..base import EnumChoice, LinkAwarePath
810

911

1012
class _Existing(StrEnum):
@@ -69,3 +71,40 @@ def cmd(existing):
6971
r = CliRunner().invoke(cmd, [])
7072
assert r.exit_code == 0, r.output
7173
assert captured["existing"] is _Existing.SKIP
74+
75+
76+
@pytest.mark.ai_generated
77+
def test_path_lexists_rejects_resolve_path():
78+
with pytest.raises(ValueError, match="resolve_path"):
79+
LinkAwarePath(lexists=True, resolve_path=True)
80+
81+
82+
@pytest.mark.ai_generated
83+
@pytest.mark.parametrize(
84+
"kwargs,broken_ok",
85+
[
86+
({"lexists": True}, True),
87+
({"lexists": True, "exists": True}, False),
88+
({"exists": True}, False),
89+
],
90+
)
91+
def test_path_lexists(tmp_path: Path, kwargs: dict, broken_ok: bool) -> None:
92+
@click.command()
93+
@click.argument("path", type=LinkAwarePath(allow_dash=True, **kwargs))
94+
def cmd(path):
95+
click.echo(f"got:{path}")
96+
97+
(tmp_path / "file.txt").write_text("content")
98+
broken = tmp_path / "broken"
99+
try:
100+
os.symlink(tmp_path / "missing", broken)
101+
except OSError:
102+
pytest.skip("symlinks are not supported here")
103+
for ok in [str(tmp_path / "file.txt"), "-"] + ([str(broken)] if broken_ok else []):
104+
r = CliRunner().invoke(cmd, [ok])
105+
assert r.exit_code == 0, r.output
106+
assert r.output == f"got:{ok}\n"
107+
for bad in [str(tmp_path / "missing")] + ([] if broken_ok else [str(broken)]):
108+
r = CliRunner().invoke(cmd, [bad])
109+
assert r.exit_code == 2
110+
assert f"Path {bad!r} does not exist." in r.output

0 commit comments

Comments
 (0)