Skip to content

Commit 5ff1d76

Browse files
committed
ui: add revision info on dvc.yaml/dvc.lock/.dvc syntax and schema errors
1 parent 582c4af commit 5ff1d76

File tree

2 files changed

+73
-18
lines changed

2 files changed

+73
-18
lines changed

dvc/utils/strictyaml.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,20 @@ def _prepare_code_snippets(
7171

7272

7373
class YAMLSyntaxError(PrettyDvcException, YAMLFileCorruptedError):
74-
def __init__(self, path: str, yaml_text: str, exc: Exception) -> None:
74+
def __init__(
75+
self,
76+
path: str,
77+
yaml_text: str,
78+
exc: Exception,
79+
rev: Optional[str] = None,
80+
) -> None:
7581
self.path: str = path
7682
self.yaml_text: str = yaml_text
7783
self.exc: Exception = exc
7884

7985
merge_conflicts = merge_conflict_marker.search(self.yaml_text)
8086
self.hint = " (possible merge conflicts)" if merge_conflicts else ""
87+
self.rev: Optional[str] = rev
8188
super().__init__(self.path)
8289

8390
def __pretty_exc__(self, **kwargs: Any) -> None:
@@ -133,7 +140,9 @@ def prepare_code(mark: "StreamMark") -> "Syntax":
133140
lines.insert(0, "")
134141

135142
rel = make_relpath(self.path)
136-
lines.insert(0, _prepare_message(f"'{rel}' is invalid{self.hint}."))
143+
rev_msg = f" in revision '{self.rev[:7]}'" if self.rev else ""
144+
msg_fmt = f"'{rel}' is invalid{self.hint}{rev_msg}."
145+
lines.insert(0, _prepare_message(msg_fmt))
137146
for line in lines:
138147
ui.error_write(line, styled=True)
139148

@@ -189,6 +198,7 @@ def __init__(
189198
exc: "MultipleInvalid",
190199
path: str = None,
191200
text: str = None,
201+
rev: str = None,
192202
) -> None:
193203
self.text = text or ""
194204
self.exc = exc
@@ -197,6 +207,7 @@ def __init__(
197207
self.path = path or ""
198208

199209
message = f"'{rel}' validation failed"
210+
message += " in revision '{}'".format(rev[:7]) if rev else ""
200211
if len(self.exc.errors) > 1:
201212
message += f": {len(self.exc.errors)} errors"
202213
super().__init__(f"{message}")
@@ -254,13 +265,14 @@ def validate(
254265
schema: Callable[[_T], _T],
255266
text: str = None,
256267
path: str = None,
268+
rev: str = None,
257269
) -> _T:
258270
from voluptuous import MultipleInvalid
259271

260272
try:
261273
return schema(data)
262274
except MultipleInvalid as exc:
263-
raise YAMLValidationError(exc, path, text) from exc
275+
raise YAMLValidationError(exc, path, text, rev=rev) from exc
264276

265277

266278
def load(
@@ -271,6 +283,8 @@ def load(
271283
round_trip: bool = False,
272284
) -> Any:
273285
open_fn = fs.open if fs else open
286+
rev = getattr(fs, "rev", None)
287+
274288
try:
275289
with open_fn(path, encoding=encoding) as fd: # type: ignore
276290
text = fd.read()
@@ -279,10 +293,10 @@ def load(
279293
raise EncodingError(path, encoding) from exc
280294
except YAMLFileCorruptedError as exc:
281295
cause = exc.__cause__
282-
raise YAMLSyntaxError(path, text, exc) from cause
296+
raise YAMLSyntaxError(path, text, exc, rev=rev) from cause
283297

284298
if schema:
285299
# not returning validated data, as it may remove
286300
# details from CommentedMap that we get from roundtrip parser
287-
validate(data, schema, text=text, path=path)
301+
validate(data, schema, text=text, path=path, rev=rev)
288302
return data, text

tests/func/utils/test_strict_yaml.py

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from ruamel.yaml import __with_libyaml__ as ruamel_clib
55

66
from dvc.cli import main
7-
from dvc.ui import ui
87

98
DUPLICATE_KEYS = """\
109
stages:
@@ -325,25 +324,37 @@
325324
}
326325

327326

328-
@pytest.mark.parametrize(
329-
"text, expected", examples.values(), ids=examples.keys()
330-
)
331-
def test_exceptions(tmp_dir, dvc, capsys, text, expected, mocker):
327+
@pytest.fixture
328+
def force_posixpath(mocker):
332329
# make it always return posix path, easier for validating error messages
333330
mocker.patch(
334-
"dvc.utils.strictyaml.make_relpath", return_value="./dvc.yaml"
331+
"dvc.utils.strictyaml.make_relpath",
332+
return_value="./dvc.yaml",
335333
)
336334

337-
console = ui.error_console
338-
original_printer = console.print
339335

340-
def print_with_fixed_width(*args, **kwargs):
341-
console.options.min_width = console.options.max_width = 80
342-
console.width = kwargs["width"] = 80
343-
return original_printer(*args, **kwargs)
336+
@pytest.fixture
337+
def fixed_width_term(mocker):
338+
"""Fixed width console."""
339+
from rich.console import Console
340+
341+
mocker.patch.object(
342+
Console, "width", new_callable=mocker.PropertyMock(return_value=80)
343+
)
344344

345-
mocker.patch.object(console, "print", print_with_fixed_width)
346345

346+
@pytest.mark.parametrize(
347+
"text, expected", examples.values(), ids=examples.keys()
348+
)
349+
def test_exceptions(
350+
tmp_dir,
351+
dvc,
352+
capsys,
353+
force_posixpath,
354+
fixed_width_term,
355+
text,
356+
expected,
357+
):
347358
tmp_dir.gen("dvc.yaml", text)
348359

349360
capsys.readouterr() # clear outputs
@@ -360,6 +371,36 @@ def print_with_fixed_width(*args, **kwargs):
360371
assert expected_line == err_line.rstrip(" ")
361372

362373

374+
@pytest.mark.parametrize(
375+
"text, expected",
376+
[
377+
(DUPLICATE_KEYS, "'./dvc.yaml' is invalid in revision '{short_rev}'."),
378+
(
379+
MISSING_CMD,
380+
"'./dvc.yaml' validation failed in revision '{short_rev}'.",
381+
),
382+
],
383+
)
384+
def test_on_revision(
385+
tmp_dir,
386+
scm,
387+
dvc,
388+
force_posixpath,
389+
fixed_width_term,
390+
capsys,
391+
text,
392+
expected,
393+
):
394+
tmp_dir.scm_gen("dvc.yaml", text, commit="add dvc.yaml")
395+
capsys.readouterr() # clear outputs
396+
397+
assert main(["ls", f"file://{tmp_dir}", "--rev", "HEAD"]) != 0
398+
399+
out, err = capsys.readouterr()
400+
assert not out
401+
assert expected.format(short_rev=scm.get_rev()[:7]) in err
402+
403+
363404
def test_make_relpath(tmp_dir, monkeypatch):
364405
from dvc.utils.strictyaml import make_relpath
365406

0 commit comments

Comments
 (0)