Skip to content

Commit 0c8c938

Browse files
authored
Merge pull request #1234 from dandi/gh-1232
Add `--ignore ID_REGEX` option to `dandi validate`
2 parents 2e117df + 335fd8f commit 0c8c938

File tree

5 files changed

+75
-42
lines changed

5 files changed

+75
-42
lines changed

dandi/cli/cmd_validate.py

+40-41
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterable
14
import logging
25
import os
3-
from typing import List, cast
6+
import re
7+
from typing import List, Optional, cast
48
import warnings
59

610
import click
711

812
from .base import devel_debug_option, devel_option, map_to_click_exceptions
913
from ..utils import pluralize
10-
from ..validate_types import Severity
14+
from ..validate_types import Severity, ValidationResult
1115

1216

1317
@click.command()
@@ -74,16 +78,18 @@ def validate_bids(
7478
type=click.Choice(["none", "path"], case_sensitive=False),
7579
default="none",
7680
)
81+
@click.option("--ignore", metavar="REGEX", help="Regex matching error IDs to ignore")
7782
@click.argument("paths", nargs=-1, type=click.Path(exists=True, dir_okay=True))
7883
@devel_debug_option()
7984
@map_to_click_exceptions
8085
def validate(
81-
paths,
82-
schema=None,
83-
devel_debug=False,
84-
allow_any_path=False,
85-
grouping="none",
86-
):
86+
paths: tuple[str, ...],
87+
ignore: Optional[str],
88+
grouping: str,
89+
schema: Optional[str] = None,
90+
devel_debug: bool = False,
91+
allow_any_path: bool = False,
92+
) -> None:
8793
"""Validate files for NWB and DANDI compliance.
8894
8995
Exits with non-0 exit code if any file is not compliant.
@@ -98,7 +104,7 @@ def validate(
98104
h.addFilter(lambda r: not getattr(r, "validating", False))
99105

100106
if not paths:
101-
paths = [os.curdir]
107+
paths = (os.curdir,)
102108
# below we are using load_namespaces but it causes HDMF to whine if there
103109
# is no cached name spaces in the file. It is benign but not really useful
104110
# at this point, so we ignore it although ideally there should be a formal
@@ -111,50 +117,44 @@ def validate(
111117
devel_debug=devel_debug,
112118
allow_any_path=allow_any_path,
113119
)
120+
_process_issues(validator_result, grouping, ignore)
114121

115-
_process_issues(validator_result, grouping)
116-
117-
118-
def _process_issues(validator_result, grouping):
119122

120-
issues = [i for i in validator_result if i.severity]
121-
122-
purviews = [
123-
list(filter(bool, [i.path, i.path_regex, i.dataset_path]))[0] for i in issues
124-
]
123+
def _process_issues(
124+
validator_result: Iterable[ValidationResult], grouping: str, ignore: Optional[str]
125+
) -> None:
126+
issues = [i for i in validator_result if i.severity is not None]
127+
if ignore is not None:
128+
issues = [i for i in issues if not re.search(ignore, i.id)]
129+
purviews = [i.purview for i in issues]
125130
if grouping == "none":
126131
display_errors(
127132
purviews,
128133
[i.id for i in issues],
129-
[i.severity for i in issues],
134+
cast(List[Severity], [i.severity for i in issues]),
130135
[i.message for i in issues],
131136
)
132137
elif grouping == "path":
133138
for purview in purviews:
134-
applies_to = [
135-
i for i in issues if purview in [i.path, i.path_regex, i.dataset_path]
136-
]
139+
applies_to = [i for i in issues if purview == i.purview]
137140
display_errors(
138141
[purview],
139142
[i.id for i in applies_to],
140-
[i.severity for i in applies_to],
143+
cast(List[Severity], [i.severity for i in applies_to]),
141144
[i.message for i in applies_to],
142145
)
143146
else:
144147
raise NotImplementedError(
145-
"The `grouping` parameter values currently supported are " "path or None."
148+
"The `grouping` parameter values currently supported are 'path' and"
149+
" 'none'."
146150
)
147-
148-
validation_errors = [i for i in issues if i.severity == Severity.ERROR]
149-
150-
if validation_errors:
151+
if any(i.severity is Severity.ERROR for i in issues):
151152
raise SystemExit(1)
152153
else:
153154
click.secho("No errors found.", fg="green")
154155

155156

156-
def _get_severity_color(severities):
157-
157+
def _get_severity_color(severities: list[Severity]) -> str:
158158
if Severity.ERROR in severities:
159159
return "red"
160160
elif Severity.WARNING in severities:
@@ -164,23 +164,22 @@ def _get_severity_color(severities):
164164

165165

166166
def display_errors(
167-
purviews: List[str],
168-
errors: List[str],
169-
severities: List[Severity],
170-
messages: List[str],
167+
purviews: list[Optional[str]],
168+
errors: list[str],
169+
severities: list[Severity],
170+
messages: list[Optional[str]],
171171
) -> None:
172172
"""
173-
Unified error display for validation CLI, which auto-resolves grouping logic based on
174-
the length of input lists.
175-
173+
Unified error display for validation CLI, which auto-resolves grouping
174+
logic based on the length of input lists.
176175
177176
Notes
178177
-----
179-
* There is a bit of roundabout and currently untestable logic to deal with potential cases
180-
where the same error has multiple error message, could be removed in the future and removed
181-
by assert if this won't ever be the case.
178+
* There is a bit of roundabout and currently untestable logic to deal with
179+
potential cases where the same error has multiple error message, could be
180+
removed in the future and removed by assert if this won't ever be the
181+
case.
182182
"""
183-
184183
if all(len(cast(list, i)) == 1 for i in [purviews, errors, severities, messages]):
185184
fg = _get_severity_color(severities)
186185
error_message = f"[{errors[0]}] {purviews[0]}{messages[0]}"

dandi/cli/tests/test_cmd_validate.py

+9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ def test_validate_nwb_error(simple3_nwb: Path) -> None:
2929
assert r.exit_code != 0
3030

3131

32+
def test_validate_ignore(simple2_nwb: Path) -> None:
33+
r = CliRunner().invoke(validate, [str(simple2_nwb)])
34+
assert r.exit_code != 0
35+
assert "DANDI.NO_DANDISET_FOUND" in r.output
36+
r = CliRunner().invoke(validate, ["--ignore=NO_DANDISET_FOUND", str(simple2_nwb)])
37+
assert r.exit_code == 0, r.output
38+
assert "DANDI.NO_DANDISET_FOUND" not in r.output
39+
40+
3241
def test_validate_bids_grouping_error(
3342
bids_error_examples: Path, dataset: str = "invalid_asl003"
3443
) -> None:

dandi/tests/test_download.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import os
3+
import os.path
34
from pathlib import Path
45
import re
56
from shutil import rmtree
@@ -311,7 +312,7 @@ def test_download_metadata404(text_dandiset: SampleDandiset, tmp_path: Path) ->
311312
errors = [s for s in statuses if s.get("status") == "error"]
312313
assert errors == [
313314
{
314-
"path": op.join(text_dandiset.dandiset_id, "subdir1", "apple.txt"),
315+
"path": os.path.join(text_dandiset.dandiset_id, "subdir1", "apple.txt"),
315316
"status": "error",
316317
"message": f"No such asset: {asset}",
317318
}

dandi/validate_types.py

+11
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,14 @@ class ValidationResult:
4949
path: Optional[Path] = None
5050
path_regex: Optional[str] = None
5151
severity: Optional[Severity] = None
52+
53+
@property
54+
def purview(self) -> Optional[str]:
55+
if self.path is not None:
56+
return str(self.path)
57+
elif self.path_regex is not None:
58+
return self.path_regex
59+
elif self.dataset_path is not None:
60+
return str(self.dataset_path)
61+
else:
62+
return None

docs/source/cmdline/validate.rst

+13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ Validate files for NWB and DANDI compliance.
99

1010
Exits with non-zero exit code if any file is not compliant.
1111

12+
Options
13+
-------
14+
15+
.. option:: -g, --grouping [none|path]
16+
17+
Set how to group reported errors & warnings: by path or not at all
18+
(default)
19+
20+
.. option:: --ignore REGEX
21+
22+
Ignore any validation errors & warnings whose ID matches the given regular
23+
expression
24+
1225

1326
Development Options
1427
-------------------

0 commit comments

Comments
 (0)