Skip to content

Commit

Permalink
parametrization: print pretty interpolation errors (iterative#4953)
Browse files Browse the repository at this point in the history
* parametrization: print pretty interpolation errors

* Fix col/loc in exception msg

* Fix tests
  • Loading branch information
skshetry authored Nov 24, 2020
1 parent bc4c9fe commit abb78ce
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 12 deletions.
24 changes: 20 additions & 4 deletions dvc/parsing/interpolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)

from dvc.exceptions import DvcException
from dvc.utils import colorize

if typing.TYPE_CHECKING:
from typing import List, Match
Expand Down Expand Up @@ -53,16 +54,31 @@ def is_interpolated_string(val):


def format_and_raise_parse_error(exc):
msg = ParseException.explain(exc, depth=0)
raise ParseError(msg)
raise ParseError(_format_exc_msg(exc))


def _format_exc_msg(exc: ParseException):
exc.loc += 2 # 2 because we append `${` at the start of expr below

expr = exc.pstr
exc.pstr = "${" + exc.pstr + "}"
error = ParseException.explain(exc, depth=0)

_, pointer, *explains = error.splitlines()
pstr = "{brace_open}{expr}{brace_close}".format(
brace_open=colorize("${", color="blue"),
expr=colorize(expr, color="magenta"),
brace_close=colorize("}", color="blue"),
)
msg = "\n".join(explains)
pointer = colorize(pointer, color="red")
return "\n".join([pstr, pointer, colorize(msg, color="red", style="bold")])


def parse_expr(s: str):
try:
result = parser.parseString(s, parseAll=True)
except ParseException as exc:
exc.pstr = "${" + exc.pstr + "}"
exc.loc += 2
format_and_raise_parse_error(exc)

joined = result.asList()
Expand Down
15 changes: 12 additions & 3 deletions dvc/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,20 +236,29 @@ def current_timestamp():
return int(nanotime.timestamp(time.time()))


def colorize(message, color=None):
def colorize(message, color=None, style=None):
"""Returns a message in a specified color."""
if not color:
return message

styles = {
"dim": colorama.Style.DIM,
"bold": colorama.Style.BRIGHT,
}

colors = {
"green": colorama.Fore.GREEN,
"yellow": colorama.Fore.YELLOW,
"blue": colorama.Fore.BLUE,
"red": colorama.Fore.RED,
"magenta": colorama.Fore.MAGENTA,
}

return "{color}{message}{nc}".format(
color=colors.get(color, ""), message=message, nc=colorama.Fore.RESET
return "{style}{color}{message}{reset}".format(
style=styles.get(style, ""),
color=colors.get(color, ""),
message=message,
reset=colorama.Style.RESET_ALL,
)


Expand Down
16 changes: 11 additions & 5 deletions tests/func/test_stage_resolver_error_msg.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re

import pytest

Expand All @@ -9,6 +10,11 @@
DATA = {"models": {"bar": "bar", "foo": "foo"}}


def escape_ansi(line):
ansi_escape = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]")
return ansi_escape.sub("", line)


@pytest.fixture
def repo(tmp_dir, dvc):
dump_yaml(tmp_dir / DEFAULT_PARAMS_FILE, DATA)
Expand Down Expand Up @@ -73,7 +79,7 @@ def test_failed_to_interpolate(tmp_dir, repo):
with pytest.raises(ResolveError) as exc_info:
resolver = DataResolver(repo, tmp_dir, d)
resolver.resolve()
assert str(exc_info.value) == (
assert escape_ansi(str(exc_info.value)) == (
"failed to parse 'stages.build.cmd' in 'dvc.yaml':\n"
"${models.foo.}\n"
" ^\n"
Expand Down Expand Up @@ -170,7 +176,7 @@ def test_foreach_interpolation_key_error(tmp_dir, repo):
resolver = DataResolver(repo, tmp_dir, d)
with pytest.raises(ResolveError) as exc_info:
resolver.resolve()
assert str(exc_info.value) == (
assert escape_ansi(str(exc_info.value)) == (
"failed to parse 'stages.build.foreach' in 'dvc.yaml':\n"
"${models[123}\n"
" ^\n"
Expand Down Expand Up @@ -201,7 +207,7 @@ def test_foreach_generated_errors(tmp_dir, repo, syn, msg):
resolver = DataResolver(repo, tmp_dir, d)
with pytest.raises(ResolveError) as exc_info:
resolver.resolve()
assert str(exc_info.value) == (
assert escape_ansi(str(exc_info.value)) == (
"failed to parse '[email protected]' in 'dvc.yaml':" + msg
)

Expand Down Expand Up @@ -260,7 +266,7 @@ def test_foreach_wdir_interpolation_issues(tmp_dir, repo, wdir, msg):
resolver = DataResolver(repo, tmp_dir, d)
with pytest.raises(ResolveError) as exc_info:
resolver.resolve()
assert str(exc_info.value) == (
assert escape_ansi(str(exc_info.value)) == (
"failed to parse '[email protected]' in 'dvc.yaml':" + msg
)

Expand Down Expand Up @@ -297,7 +303,7 @@ def test_wdir_failed_to_interpolate(tmp_dir, repo, wdir, expected_msg):
resolver = DataResolver(repo, tmp_dir, d)
with pytest.raises(ResolveError) as exc_info:
resolver.resolve()
assert str(exc_info.value) == (
assert escape_ansi(str(exc_info.value)) == (
"failed to parse 'stages.build.wdir' in 'dvc.yaml':" + expected_msg
)

Expand Down

0 comments on commit abb78ce

Please sign in to comment.