mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-09 23:10:24 +01:00
plots: add plot markers to DVC files (#3807)
* run: add --plot/--plot-no-cache Part of #3409 * plot: move show/diff to API * plots: show marked plots by default Involves some refactoring to make plots comply with the rest of the code base. * dvc.yaml: use top-level metrics and plots * dvc.yaml: add support for plot template * plots: add support for templates in dvc.yaml * dvc.yaml: allow metrics/plots to have persist/cache * plots: render single page by-default and introduce --show-json * plot: use "working tree" instead of "workspace" Just to sync this with the rest of the codebase. We should consider renaming it to "workspace" everywhere, but for now I opt to not touch it to not break something that depends on it. * plots: rename -f|--file to -o|--out Better corresponds to other commands like `import`. * plots: use RepoTree and brancher instead of api * stage: get rid of OutputParams * tests: plots: add simple diff test * plots: use `--targets` instead of `--datafile` Makes it comply with `dvc metrics/params` CLI options. * tests: unit: plots: add --show-json test * stage: cleanup exception for format error Based on feedback from the users. * lockfile: use relpath in corruption error
This commit is contained in:
+56
-72
@@ -4,37 +4,33 @@ import os
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo.plots.data import WORKSPACE_REVISION_NAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAGE_HTML = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DVC Plot</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
</head>
|
||||
<body>
|
||||
{divs}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
DIV_HTML = """<div id = "{id}"></div>
|
||||
<script type = "text/javascript">
|
||||
var spec = {vega_json};
|
||||
vegaEmbed('#{id}', spec);
|
||||
</script>"""
|
||||
|
||||
|
||||
class CmdPlots(CmdBase):
|
||||
def _revisions(self):
|
||||
def _func(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def _result_file(self):
|
||||
if self.args.file:
|
||||
return self.args.file
|
||||
|
||||
extension = self._result_extension()
|
||||
base = self._result_basename()
|
||||
|
||||
result_file = base + extension
|
||||
return result_file
|
||||
|
||||
def _result_basename(self):
|
||||
if self.args.datafile:
|
||||
return self.args.datafile
|
||||
return "plot"
|
||||
|
||||
def _result_extension(self):
|
||||
if not self.args.no_html:
|
||||
return ".html"
|
||||
elif self.args.template:
|
||||
return os.path.splitext(self.args.template)[-1]
|
||||
return ".json"
|
||||
|
||||
def run(self):
|
||||
fields = None
|
||||
jsonpath = None
|
||||
@@ -44,33 +40,38 @@ class CmdPlots(CmdBase):
|
||||
else:
|
||||
fields = set(self.args.select.split(","))
|
||||
try:
|
||||
plot_string = self.repo.plot(
|
||||
datafile=self.args.datafile,
|
||||
plots = self._func(
|
||||
targets=self.args.targets,
|
||||
template=self.args.template,
|
||||
revisions=self._revisions(),
|
||||
fields=fields,
|
||||
x_field=self.args.x,
|
||||
y_field=self.args.y,
|
||||
path=jsonpath,
|
||||
embed=not self.args.no_html,
|
||||
csv_header=not self.args.no_csv_header,
|
||||
title=self.args.title,
|
||||
x_title=self.args.xlab,
|
||||
y_title=self.args.ylab,
|
||||
)
|
||||
|
||||
if self.args.stdout:
|
||||
logger.info(plot_string)
|
||||
else:
|
||||
result_path = self._result_file()
|
||||
with open(result_path, "w") as fobj:
|
||||
fobj.write(plot_string)
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(
|
||||
"file://{}".format(
|
||||
os.path.join(self.repo.root_dir, result_path)
|
||||
)
|
||||
)
|
||||
logger.info(json.dumps(plots))
|
||||
return 0
|
||||
|
||||
divs = [
|
||||
DIV_HTML.format(id=f"plot{i}", vega_json=plot)
|
||||
for i, plot in enumerate(plots.values())
|
||||
]
|
||||
html = PAGE_HTML.format(divs="\n".join(divs))
|
||||
path = self.args.out or "plots.html"
|
||||
|
||||
with open(path, "w") as fobj:
|
||||
fobj.write(html)
|
||||
|
||||
logger.info(
|
||||
"file://{}".format(os.path.join(self.repo.root_dir, path))
|
||||
)
|
||||
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
@@ -80,18 +81,13 @@ class CmdPlots(CmdBase):
|
||||
|
||||
|
||||
class CmdPlotsShow(CmdPlots):
|
||||
def _revisions(self):
|
||||
return None
|
||||
def _func(self, *args, **kwargs):
|
||||
return self.repo.plots.show(*args, **kwargs)
|
||||
|
||||
|
||||
class CmdPlotsDiff(CmdPlots):
|
||||
def _revisions(self):
|
||||
revisions = self.args.revisions or []
|
||||
if len(revisions) <= 1:
|
||||
if len(revisions) == 0 and self.repo.scm.is_dirty():
|
||||
revisions.append("HEAD")
|
||||
revisions.append(WORKSPACE_REVISION_NAME)
|
||||
return revisions
|
||||
def _func(self, *args, **kwargs):
|
||||
return self.repo.plots.diff(*args, revs=self.args.revisions, **kwargs)
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
@@ -130,7 +126,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help="File to be injected with data.",
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"-f", "--file", default=None, help="Name of the generated file."
|
||||
"-o", "--out", default=None, help="Destination path to save plots to.",
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"-s",
|
||||
@@ -144,12 +140,6 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_show_parser.add_argument(
|
||||
"-y", default=None, help="Field name for y axis."
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"--stdout",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print plots specification to stdout.",
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"--no-csv-header",
|
||||
action="store_true",
|
||||
@@ -157,10 +147,10 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Required when CSV or TSV datafile does not have a header.",
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"--no-html",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Do not wrap Vega plot JSON with HTML.",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
plots_show_parser.add_argument("--title", default=None, help="Plot title.")
|
||||
plots_show_parser.add_argument(
|
||||
@@ -170,7 +160,9 @@ def add_parser(subparsers, parent_parser):
|
||||
"--ylab", default=None, help="Y axis title."
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"datafile", nargs="?", default=None, help="Metrics file to visualize",
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Metrics files to visualize. Shows all plots by default.",
|
||||
)
|
||||
plots_show_parser.set_defaults(func=CmdPlotsShow)
|
||||
|
||||
@@ -193,14 +185,12 @@ def add_parser(subparsers, parent_parser):
|
||||
help="File to be injected with data.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"-d",
|
||||
"--datafile",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help="Metrics file to visualize",
|
||||
"--targets",
|
||||
nargs="*",
|
||||
help="Metrics file to visualize. Shows all plots by default.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"-f", "--file", default=None, help="Name of the generated file."
|
||||
"-o", "--out", default=None, help="Destination path to save plots to.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"-s",
|
||||
@@ -214,12 +204,6 @@ def add_parser(subparsers, parent_parser):
|
||||
plots_diff_parser.add_argument(
|
||||
"-y", default=None, help="Field name for y axis."
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--stdout",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print plot specification to stdout.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--no-csv-header",
|
||||
action="store_true",
|
||||
@@ -227,10 +211,10 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Provided CSV ot TSV datafile does not have a header.",
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--no-html",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Do not wrap Vega plot JSON with HTML.",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
plots_diff_parser.add_argument("--title", default=None, help="Plot title.")
|
||||
plots_diff_parser.add_argument(
|
||||
|
||||
+20
-2
@@ -16,6 +16,8 @@ class CmdRun(CmdBase):
|
||||
self.args.outs_no_cache,
|
||||
self.args.metrics,
|
||||
self.args.metrics_no_cache,
|
||||
self.args.plots,
|
||||
self.args.plots_no_cache,
|
||||
self.args.outs_persist,
|
||||
self.args.outs_persist_no_cache,
|
||||
self.args.params,
|
||||
@@ -24,8 +26,8 @@ class CmdRun(CmdBase):
|
||||
): # pragma: no cover
|
||||
logger.error(
|
||||
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
|
||||
"`-m`, `-M`, `-p`, `--outs-persist`, "
|
||||
"`--outs-persist-no-cache`, `command`."
|
||||
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
|
||||
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
|
||||
)
|
||||
return 1
|
||||
|
||||
@@ -36,6 +38,8 @@ class CmdRun(CmdBase):
|
||||
outs_no_cache=self.args.outs_no_cache,
|
||||
metrics=self.args.metrics,
|
||||
metrics_no_cache=self.args.metrics_no_cache,
|
||||
plots=self.args.plots,
|
||||
plots_no_cache=self.args.plots_no_cache,
|
||||
deps=self.args.deps,
|
||||
params=self.args.params,
|
||||
fname=self.args.file,
|
||||
@@ -140,6 +144,20 @@ def add_parser(subparsers, parent_parser):
|
||||
"(do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--plots",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output plot file.",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--plots-no-cache",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output plot file (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
|
||||
+7
-6
@@ -32,8 +32,7 @@ PIPELINE_LOCK = "dvc.lock"
|
||||
|
||||
|
||||
class LockfileCorruptedError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(f"Lockfile '{path}' is corrupted.")
|
||||
pass
|
||||
|
||||
|
||||
def is_valid_filename(path):
|
||||
@@ -103,7 +102,7 @@ class FileMixin:
|
||||
with self.repo.tree.open(self.path) as fd:
|
||||
stage_text = fd.read()
|
||||
d = parse_stage(stage_text, self.path)
|
||||
self.validate(d, self.path)
|
||||
self.validate(d, self.relpath)
|
||||
return d, stage_text
|
||||
|
||||
@classmethod
|
||||
@@ -112,7 +111,7 @@ class FileMixin:
|
||||
try:
|
||||
cls.SCHEMA(d)
|
||||
except MultipleInvalid as exc:
|
||||
raise StageFileFormatError(fname, exc)
|
||||
raise StageFileFormatError(f"'{fname}' format error: {exc}")
|
||||
|
||||
def remove_with_prompt(self, force=False):
|
||||
raise NotImplementedError
|
||||
@@ -238,9 +237,11 @@ class Lockfile(FileMixin):
|
||||
with self.repo.tree.open(self.path) as fd:
|
||||
data = parse_stage(fd.read(), self.path)
|
||||
try:
|
||||
self.validate(data, fname=self.path)
|
||||
self.validate(data, fname=self.relpath)
|
||||
except StageFileFormatError:
|
||||
raise LockfileCorruptedError(self.path)
|
||||
raise LockfileCorruptedError(
|
||||
f"Lockfile '{self.relpath}' is corrupted."
|
||||
)
|
||||
return data
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
|
||||
+23
-4
@@ -54,10 +54,11 @@ SCHEMA = CHECKSUMS_SCHEMA.copy()
|
||||
SCHEMA[Required(BaseOutput.PARAM_PATH)] = str
|
||||
SCHEMA[BaseOutput.PARAM_CACHE] = bool
|
||||
SCHEMA[BaseOutput.PARAM_METRIC] = BaseOutput.METRIC_SCHEMA
|
||||
SCHEMA[BaseOutput.PARAM_PLOT] = bool
|
||||
SCHEMA[BaseOutput.PARAM_PERSIST] = bool
|
||||
|
||||
|
||||
def _get(stage, p, info, cache, metric, persist=False):
|
||||
def _get(stage, p, info, cache, metric, plot=False, persist=False):
|
||||
parsed = urlparse(p)
|
||||
|
||||
if parsed.scheme == "remote":
|
||||
@@ -69,6 +70,7 @@ def _get(stage, p, info, cache, metric, persist=False):
|
||||
cache=cache,
|
||||
remote=remote,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
)
|
||||
|
||||
@@ -81,6 +83,7 @@ def _get(stage, p, info, cache, metric, persist=False):
|
||||
cache=cache,
|
||||
remote=None,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
)
|
||||
return LocalOutput(
|
||||
@@ -90,6 +93,7 @@ def _get(stage, p, info, cache, metric, persist=False):
|
||||
cache=cache,
|
||||
remote=None,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
)
|
||||
|
||||
@@ -100,19 +104,34 @@ def loadd_from(stage, d_list):
|
||||
p = d.pop(BaseOutput.PARAM_PATH)
|
||||
cache = d.pop(BaseOutput.PARAM_CACHE, True)
|
||||
metric = d.pop(BaseOutput.PARAM_METRIC, False)
|
||||
plot = d.pop(BaseOutput.PARAM_PLOT, False)
|
||||
persist = d.pop(BaseOutput.PARAM_PERSIST, False)
|
||||
ret.append(
|
||||
_get(
|
||||
stage, p, info=d, cache=cache, metric=metric, persist=persist,
|
||||
stage,
|
||||
p,
|
||||
info=d,
|
||||
cache=cache,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
def loads_from(stage, s_list, use_cache=True, metric=False, persist=False):
|
||||
def loads_from(
|
||||
stage, s_list, use_cache=True, metric=False, plot=False, persist=False
|
||||
):
|
||||
return [
|
||||
_get(
|
||||
stage, s, info={}, cache=use_cache, metric=metric, persist=persist,
|
||||
stage,
|
||||
s,
|
||||
info={},
|
||||
cache=use_cache,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
)
|
||||
for s in s_list
|
||||
]
|
||||
|
||||
+9
-1
@@ -50,6 +50,8 @@ class BaseOutput:
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
PARAM_METRIC_XPATH = "xpath"
|
||||
PARAM_PLOT = "plot"
|
||||
PARAM_PLOT_TEMPLATE = "template"
|
||||
PARAM_PERSIST = "persist"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
@@ -75,6 +77,7 @@ class BaseOutput:
|
||||
remote=None,
|
||||
cache=True,
|
||||
metric=False,
|
||||
plot=False,
|
||||
persist=False,
|
||||
):
|
||||
self._validate_output_path(path)
|
||||
@@ -95,6 +98,7 @@ class BaseOutput:
|
||||
self.remote = remote or self.REMOTE(self.repo, {})
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
|
||||
self.path_info = self._parse_path(remote, path)
|
||||
@@ -234,7 +238,7 @@ class BaseOutput:
|
||||
|
||||
if not self.use_cache:
|
||||
self.info = self.save_info()
|
||||
if self.metric:
|
||||
if self.metric or self.plot:
|
||||
self.verify_metric()
|
||||
if not self.IS_DEPENDENCY:
|
||||
logger.info(
|
||||
@@ -273,6 +277,10 @@ class BaseOutput:
|
||||
del self.metric[self.PARAM_METRIC_XPATH]
|
||||
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
return ret
|
||||
|
||||
+6
-5
@@ -71,17 +71,18 @@ class LocalOutput(BaseOutput):
|
||||
return ret
|
||||
|
||||
def verify_metric(self):
|
||||
if not self.metric:
|
||||
if not self.metric or self.plot:
|
||||
return
|
||||
|
||||
path = os.fspath(self.path_info)
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
name = "metrics" if self.metric else "plot"
|
||||
if os.path.isdir(path):
|
||||
msg = "directory '{}' cannot be used as metrics."
|
||||
raise DvcException(msg.format(self.path_info))
|
||||
msg = "directory '{}' cannot be used as {}."
|
||||
raise DvcException(msg.format(self.path_info, name))
|
||||
|
||||
if not istextfile(path):
|
||||
msg = "binary file '{}' cannot be used as metrics."
|
||||
raise DvcException(msg.format(self.path_info))
|
||||
msg = "binary file '{}' cannot be used as {}."
|
||||
raise DvcException(msg.format(self.path_info, name))
|
||||
|
||||
@@ -56,7 +56,6 @@ class Repo:
|
||||
from dvc.repo.get import get
|
||||
from dvc.repo.get_url import get_url
|
||||
from dvc.repo.update import update
|
||||
from dvc.repo.plots import plot
|
||||
|
||||
def __init__(self, root_dir=None):
|
||||
from dvc.state import State
|
||||
@@ -65,6 +64,7 @@ class Repo:
|
||||
from dvc.cache import Cache
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.repo.metrics import Metrics
|
||||
from dvc.repo.plots import Plots
|
||||
from dvc.repo.params import Params
|
||||
from dvc.scm.tree import WorkingTree
|
||||
from dvc.utils.fs import makedirs
|
||||
@@ -104,6 +104,7 @@ class Repo:
|
||||
self.stage_cache = StageCache(self)
|
||||
|
||||
self.metrics = Metrics(self)
|
||||
self.plots = Plots(self)
|
||||
self.params = Params(self)
|
||||
|
||||
self._ignore()
|
||||
|
||||
@@ -25,13 +25,16 @@ def brancher( # noqa: E302
|
||||
return
|
||||
|
||||
saved_tree = self.tree
|
||||
revs = revs or []
|
||||
revs = revs.copy() if revs else []
|
||||
|
||||
scm = self.scm
|
||||
|
||||
self.tree = WorkingTree(self.root_dir)
|
||||
yield "working tree"
|
||||
|
||||
if revs and "working tree" in revs:
|
||||
revs.remove("working tree")
|
||||
|
||||
if all_commits:
|
||||
revs = scm.list_all_commits()
|
||||
else:
|
||||
|
||||
+9
-162
@@ -1,166 +1,13 @@
|
||||
import logging
|
||||
import os
|
||||
class Plots:
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
from funcy import first, last
|
||||
def show(self, *args, **kwargs):
|
||||
from .show import show
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import locked
|
||||
return show(self.repo, *args, **kwargs)
|
||||
|
||||
from .data import PlotData
|
||||
from .template import NoDataForTemplateError, Template
|
||||
def diff(self, *args, **kwargs):
|
||||
from .diff import diff
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAGE_HTML = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DVC Plot</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
</head>
|
||||
<body>
|
||||
{divs}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
DIV_HTML = """<div id = "{id}"></div>
|
||||
<script type = "text/javascript">
|
||||
var spec = {vega_json};
|
||||
vegaEmbed('#{id}', spec);
|
||||
</script>"""
|
||||
|
||||
|
||||
class TooManyDataSourcesError(DvcException):
|
||||
def __init__(self, datafile, template_datafiles):
|
||||
super().__init__(
|
||||
"Unable to infer which of possible data sources: '{}' "
|
||||
"should be replaced with '{}'.".format(
|
||||
", ".join(template_datafiles), datafile
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NoDataOrTemplateProvided(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__("Datafile or template is not specified.")
|
||||
|
||||
|
||||
def _evaluate_templatepath(repo, template=None):
|
||||
if not template:
|
||||
return repo.plot_templates.default_template
|
||||
|
||||
if os.path.exists(template):
|
||||
return template
|
||||
return repo.plot_templates.get_template(template)
|
||||
|
||||
|
||||
@locked
|
||||
def fill_template(
|
||||
repo,
|
||||
datafile,
|
||||
template_path,
|
||||
revisions,
|
||||
fields=None,
|
||||
path=None,
|
||||
csv_header=True,
|
||||
x_field=None,
|
||||
y_field=None,
|
||||
**kwargs
|
||||
):
|
||||
if x_field and fields:
|
||||
fields.add(x_field)
|
||||
|
||||
if y_field and fields:
|
||||
fields.add(y_field)
|
||||
|
||||
template_datafiles, x_anchor, y_anchor = _parse_template(
|
||||
template_path, datafile
|
||||
)
|
||||
append_index = x_anchor and not x_field
|
||||
if append_index:
|
||||
x_field = PlotData.INDEX_FIELD
|
||||
|
||||
template_data = {}
|
||||
for template_datafile in template_datafiles:
|
||||
from .data import _load_from_revisions
|
||||
|
||||
plot_datas = _load_from_revisions(repo, template_datafile, revisions)
|
||||
tmp_data = []
|
||||
for pd in plot_datas:
|
||||
rev_data_points = pd.to_datapoints(
|
||||
fields=fields,
|
||||
path=path,
|
||||
csv_header=csv_header,
|
||||
append_index=append_index,
|
||||
)
|
||||
|
||||
if y_anchor and not y_field:
|
||||
y_field = _infer_y_field(rev_data_points, x_field)
|
||||
tmp_data.extend(rev_data_points)
|
||||
|
||||
template_data[template_datafile] = tmp_data
|
||||
|
||||
if len(template_data) == 0:
|
||||
raise NoDataForTemplateError(template_path)
|
||||
|
||||
return Template.fill(
|
||||
template_path,
|
||||
template_data,
|
||||
priority_datafile=datafile,
|
||||
x_field=x_field,
|
||||
y_field=y_field,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def _infer_y_field(rev_data_points, x_field):
|
||||
all_fields = list(first(rev_data_points).keys())
|
||||
all_fields.remove(PlotData.REVISION_FIELD)
|
||||
if x_field and x_field in all_fields:
|
||||
all_fields.remove(x_field)
|
||||
y_field = last(all_fields)
|
||||
return y_field
|
||||
|
||||
|
||||
def plot(
|
||||
repo, datafile=None, template=None, revisions=None, embed=False, **kwargs
|
||||
):
|
||||
if revisions is None:
|
||||
from .data import WORKSPACE_REVISION_NAME
|
||||
|
||||
revisions = [WORKSPACE_REVISION_NAME]
|
||||
|
||||
if not datafile and not template:
|
||||
raise NoDataOrTemplateProvided()
|
||||
|
||||
template_path = _evaluate_templatepath(repo, template)
|
||||
|
||||
plot_content = fill_template(
|
||||
repo, datafile, template_path, revisions, **kwargs
|
||||
)
|
||||
|
||||
if embed:
|
||||
div = DIV_HTML.format(id="plot", vega_json=plot_content)
|
||||
plot_content = PAGE_HTML.format(divs=div)
|
||||
|
||||
return plot_content
|
||||
|
||||
|
||||
def _parse_template(template_path, priority_datafile):
|
||||
with open(template_path) as fobj:
|
||||
tempalte_content = fobj.read()
|
||||
|
||||
template_datafiles = Template.parse_data_anchors(tempalte_content)
|
||||
if priority_datafile:
|
||||
if len(template_datafiles) > 1:
|
||||
raise TooManyDataSourcesError(
|
||||
priority_datafile, template_datafiles
|
||||
)
|
||||
template_datafiles = {priority_datafile}
|
||||
|
||||
return (
|
||||
template_datafiles,
|
||||
Template.X_ANCHOR in tempalte_content,
|
||||
Template.Y_ANCHOR in tempalte_content,
|
||||
)
|
||||
return diff(self.repo, *args, **kwargs)
|
||||
|
||||
+7
-14
@@ -14,8 +14,6 @@ from dvc.exceptions import DvcException, PathMissingError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
WORKSPACE_REVISION_NAME = "workspace"
|
||||
|
||||
|
||||
class PlotMetricTypeError(DvcException):
|
||||
def __init__(self, file):
|
||||
@@ -241,20 +239,12 @@ class YAMLPlotData(PlotData):
|
||||
|
||||
|
||||
def _load_from_revision(repo, datafile, revision):
|
||||
if revision is WORKSPACE_REVISION_NAME:
|
||||
from dvc.repo.tree import RepoTree
|
||||
|
||||
def open_datafile():
|
||||
return repo.tree.open(datafile, "r")
|
||||
|
||||
else:
|
||||
|
||||
def open_datafile():
|
||||
from dvc import api
|
||||
|
||||
return api.open(datafile, repo.root_dir, revision)
|
||||
tree = RepoTree(repo)
|
||||
|
||||
try:
|
||||
with open_datafile() as fobj:
|
||||
with tree.open(datafile) as fobj:
|
||||
datafile_content = fobj.read()
|
||||
|
||||
except (FileNotFoundError, PathMissingError):
|
||||
@@ -267,7 +257,10 @@ def _load_from_revisions(repo, datafile, revisions):
|
||||
data = []
|
||||
exceptions = []
|
||||
|
||||
for rev in revisions:
|
||||
for rev in repo.brancher(revs=revisions):
|
||||
if rev == "working tree" and rev not in revisions:
|
||||
continue
|
||||
|
||||
try:
|
||||
data.append(_load_from_revision(repo, datafile, rev))
|
||||
except NoMetricOnRevisionError as e:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
def _revisions(revs, is_dirty):
|
||||
revisions = revs or []
|
||||
if len(revisions) <= 1:
|
||||
if len(revisions) == 0 and is_dirty:
|
||||
revisions.append("HEAD")
|
||||
revisions.append("working tree")
|
||||
return revisions
|
||||
|
||||
|
||||
def diff(repo, *args, revs=None, **kwargs):
|
||||
return repo.plots.show(
|
||||
*args, revs=_revisions(revs, repo.scm.is_dirty()), **kwargs
|
||||
)
|
||||
@@ -0,0 +1,214 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
from funcy import first, last
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import locked
|
||||
|
||||
from .data import PlotData
|
||||
from .template import NoDataForTemplateError, Template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TooManyDataSourcesError(DvcException):
|
||||
def __init__(self, datafile, template_datafiles):
|
||||
super().__init__(
|
||||
"Unable to infer which of possible data sources: '{}' "
|
||||
"should be replaced with '{}'.".format(
|
||||
", ".join(template_datafiles), datafile
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class NoDataOrTemplateProvided(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__("Datafile or template is not specified.")
|
||||
|
||||
|
||||
class TooManyTemplatesError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
def _evaluate_templatepath(repo, template=None):
|
||||
if not template:
|
||||
return repo.plot_templates.default_template
|
||||
|
||||
if os.path.exists(template):
|
||||
return template
|
||||
return repo.plot_templates.get_template(template)
|
||||
|
||||
|
||||
@locked
|
||||
def fill_template(
|
||||
repo,
|
||||
datafile,
|
||||
template_path,
|
||||
revisions,
|
||||
fields=None,
|
||||
path=None,
|
||||
csv_header=True,
|
||||
x_field=None,
|
||||
y_field=None,
|
||||
**kwargs,
|
||||
):
|
||||
if x_field and fields:
|
||||
fields.add(x_field)
|
||||
|
||||
if y_field and fields:
|
||||
fields.add(y_field)
|
||||
|
||||
template_datafiles, x_anchor, y_anchor = _parse_template(
|
||||
template_path, datafile
|
||||
)
|
||||
append_index = x_anchor and not x_field
|
||||
if append_index:
|
||||
x_field = PlotData.INDEX_FIELD
|
||||
|
||||
template_data = {}
|
||||
for template_datafile in template_datafiles:
|
||||
from .data import _load_from_revisions
|
||||
|
||||
plot_datas = _load_from_revisions(repo, template_datafile, revisions)
|
||||
tmp_data = []
|
||||
for pd in plot_datas:
|
||||
rev_data_points = pd.to_datapoints(
|
||||
fields=fields,
|
||||
path=path,
|
||||
csv_header=csv_header,
|
||||
append_index=append_index,
|
||||
)
|
||||
|
||||
if y_anchor and not y_field:
|
||||
y_field = _infer_y_field(rev_data_points, x_field)
|
||||
tmp_data.extend(rev_data_points)
|
||||
|
||||
template_data[template_datafile] = tmp_data
|
||||
|
||||
if len(template_data) == 0:
|
||||
raise NoDataForTemplateError(template_path)
|
||||
|
||||
content = Template.fill(
|
||||
template_path,
|
||||
template_data,
|
||||
priority_datafile=datafile,
|
||||
x_field=x_field,
|
||||
y_field=y_field,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
path = datafile or ",".join(template_datafiles)
|
||||
|
||||
return path, content
|
||||
|
||||
|
||||
def _infer_y_field(rev_data_points, x_field):
|
||||
all_fields = list(first(rev_data_points).keys())
|
||||
all_fields.remove(PlotData.REVISION_FIELD)
|
||||
if x_field and x_field in all_fields:
|
||||
all_fields.remove(x_field)
|
||||
y_field = last(all_fields)
|
||||
return y_field
|
||||
|
||||
|
||||
def _show(repo, datafile=None, template=None, revs=None, **kwargs):
|
||||
if revs is None:
|
||||
revs = ["working tree"]
|
||||
|
||||
if not datafile and not template:
|
||||
raise NoDataOrTemplateProvided()
|
||||
|
||||
template_path = _evaluate_templatepath(repo, template)
|
||||
|
||||
plot_datafile, plot_content = fill_template(
|
||||
repo, datafile, template_path, revs, **kwargs
|
||||
)
|
||||
|
||||
return plot_datafile, plot_content
|
||||
|
||||
|
||||
def _parse_template(template_path, priority_datafile):
|
||||
with open(template_path) as fobj:
|
||||
tempalte_content = fobj.read()
|
||||
|
||||
template_datafiles = Template.parse_data_anchors(tempalte_content)
|
||||
if priority_datafile:
|
||||
if len(template_datafiles) > 1:
|
||||
raise TooManyDataSourcesError(
|
||||
priority_datafile, template_datafiles
|
||||
)
|
||||
template_datafiles = {priority_datafile}
|
||||
|
||||
return (
|
||||
template_datafiles,
|
||||
Template.X_ANCHOR in tempalte_content,
|
||||
Template.Y_ANCHOR in tempalte_content,
|
||||
)
|
||||
|
||||
|
||||
def _collect_plots(repo):
|
||||
plots = defaultdict(set)
|
||||
|
||||
for stage in repo.stages:
|
||||
for out in stage.outs:
|
||||
if not out.plot:
|
||||
continue
|
||||
|
||||
if isinstance(out.plot, dict):
|
||||
template = out.plot[out.PARAM_PLOT_TEMPLATE]
|
||||
else:
|
||||
template = None
|
||||
|
||||
plots[str(out)].add(template)
|
||||
|
||||
return plots
|
||||
|
||||
|
||||
def show(repo, targets=None, template=None, revs=None, **kwargs) -> dict:
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
|
||||
if targets:
|
||||
for target in targets:
|
||||
return {
|
||||
target: _show(
|
||||
repo,
|
||||
datafile=target,
|
||||
template=template,
|
||||
revs=revs,
|
||||
**kwargs,
|
||||
)[1]
|
||||
}
|
||||
|
||||
if not revs:
|
||||
plots = _collect_plots(repo)
|
||||
else:
|
||||
plots = defaultdict(set)
|
||||
for rev in repo.brancher(revs=revs):
|
||||
for plot, templates in _collect_plots(repo).items():
|
||||
plots[plot].update(templates)
|
||||
|
||||
if not plots:
|
||||
datafile, plot = _show(
|
||||
repo, datafile=None, template=template, revs=revs, **kwargs
|
||||
)
|
||||
return {datafile: plot}
|
||||
|
||||
ret = {}
|
||||
for plot, templates in plots.items():
|
||||
tmplt = template
|
||||
if len(templates) == 1:
|
||||
tmplt = list(templates)[0]
|
||||
elif not template:
|
||||
raise TooManyTemplatesError(
|
||||
f"'{plot}' uses multiple templates '{templates}'. "
|
||||
"Use `-t|--template` to specify the template to use. "
|
||||
)
|
||||
|
||||
ret[plot] = _show(
|
||||
repo, datafile=plot, template=tmplt, revs=revs, **kwargs
|
||||
)[1]
|
||||
|
||||
return ret
|
||||
@@ -25,6 +25,8 @@ def _get_file_path(kwargs):
|
||||
kwargs.get("outs_no_cache", []),
|
||||
kwargs.get("metrics", []),
|
||||
kwargs.get("metrics_no_cache", []),
|
||||
kwargs.get("plots", []),
|
||||
kwargs.get("plots_no_cache", []),
|
||||
kwargs.get("outs_persist", []),
|
||||
kwargs.get("outs_persist_no_cache", []),
|
||||
)
|
||||
|
||||
+9
-5
@@ -26,12 +26,12 @@ LOCK_FILE_STAGE_SCHEMA = {
|
||||
LOCKFILE_SCHEMA = {str: LOCK_FILE_STAGE_SCHEMA}
|
||||
|
||||
OUT_PSTAGE_DETAILED_SCHEMA = {
|
||||
str: {
|
||||
BaseOutput.PARAM_CACHE: bool,
|
||||
BaseOutput.PARAM_PERSIST: bool,
|
||||
BaseOutput.PARAM_METRIC: bool,
|
||||
}
|
||||
str: {BaseOutput.PARAM_CACHE: bool, BaseOutput.PARAM_PERSIST: bool}
|
||||
}
|
||||
|
||||
PLOT_PSTAGE_SCHEMA = OUT_PSTAGE_DETAILED_SCHEMA.copy()
|
||||
PLOT_PSTAGE_SCHEMA[str][BaseOutput.PARAM_PLOT_TEMPLATE] = str
|
||||
|
||||
PARAM_PSTAGE_NON_DEFAULT_SCHEMA = {str: [str]}
|
||||
|
||||
SINGLE_PIPELINE_STAGE_SCHEMA = {
|
||||
@@ -48,6 +48,10 @@ SINGLE_PIPELINE_STAGE_SCHEMA = {
|
||||
Optional(StageParams.PARAM_OUTS): [
|
||||
Any(str, OUT_PSTAGE_DETAILED_SCHEMA)
|
||||
],
|
||||
Optional(StageParams.PARAM_METRICS): [
|
||||
Any(str, OUT_PSTAGE_DETAILED_SCHEMA)
|
||||
],
|
||||
Optional(StageParams.PARAM_PLOTS): [Any(str, PLOT_PSTAGE_SCHEMA)],
|
||||
}
|
||||
}
|
||||
MULTI_STAGE_SCHEMA = {STAGES: SINGLE_PIPELINE_STAGE_SCHEMA}
|
||||
|
||||
+18
-4
@@ -23,6 +23,7 @@ PARAM_OUTS = StageParams.PARAM_OUTS
|
||||
|
||||
PARAM_CACHE = BaseOutput.PARAM_CACHE
|
||||
PARAM_METRIC = BaseOutput.PARAM_METRIC
|
||||
PARAM_PLOT = BaseOutput.PARAM_PLOT
|
||||
PARAM_PERSIST = BaseOutput.PARAM_PERSIST
|
||||
|
||||
DEFAULT_PARAMS_FILE = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
@@ -35,10 +36,10 @@ def _get_out(out):
|
||||
res = OrderedDict()
|
||||
if not out.use_cache:
|
||||
res[PARAM_CACHE] = False
|
||||
if out.metric:
|
||||
res[PARAM_METRIC] = True
|
||||
elif out.persist:
|
||||
if out.persist:
|
||||
res[PARAM_PERSIST] = True
|
||||
if out.plot and isinstance(out.plot, dict):
|
||||
res.update(out.plot)
|
||||
return out.def_path if not res else {out.def_path: res}
|
||||
|
||||
|
||||
@@ -92,7 +93,20 @@ def to_pipeline_file(stage: "PipelineStage"):
|
||||
(stage.PARAM_WDIR, resolve_wdir(stage.wdir, stage.path)),
|
||||
(stage.PARAM_DEPS, sorted([d.def_path for d in deps])),
|
||||
(stage.PARAM_PARAMS, serialized_params),
|
||||
(PARAM_OUTS, _get_outs(stage.outs)),
|
||||
(
|
||||
PARAM_OUTS,
|
||||
_get_outs(
|
||||
[out for out in stage.outs if not (out.metric or out.plot)]
|
||||
),
|
||||
),
|
||||
(
|
||||
stage.PARAM_METRICS,
|
||||
_get_outs([out for out in stage.outs if out.metric]),
|
||||
),
|
||||
(
|
||||
stage.PARAM_PLOTS,
|
||||
_get_outs([out for out in stage.outs if out.plot]),
|
||||
),
|
||||
(stage.PARAM_LOCKED, stage.locked),
|
||||
(stage.PARAM_ALWAYS_CHANGED, stage.always_changed),
|
||||
]
|
||||
|
||||
@@ -10,9 +10,7 @@ class StageCmdFailedError(DvcException):
|
||||
|
||||
|
||||
class StageFileFormatError(DvcException):
|
||||
def __init__(self, fname, e):
|
||||
msg = "DVC-file '{}' format error: {}".format(fname, str(e))
|
||||
super().__init__(msg)
|
||||
pass
|
||||
|
||||
|
||||
class StageFileDoesNotExistError(DvcException):
|
||||
|
||||
+34
-8
@@ -122,18 +122,42 @@ class StageLoader(Mapping):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _load_outs(cls, stage, data):
|
||||
def _load_outs(cls, stage, data, typ=None):
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
d = []
|
||||
for key in data:
|
||||
extra_kwargs = {}
|
||||
if isinstance(key, str):
|
||||
path = key
|
||||
elif isinstance(key, dict):
|
||||
path = first(key)
|
||||
extra_kwargs = key[path]
|
||||
else:
|
||||
entry = {BaseOutput.PARAM_PATH: key}
|
||||
if typ:
|
||||
entry[typ] = True
|
||||
d.append(entry)
|
||||
continue
|
||||
d.append({"path": path, **extra_kwargs})
|
||||
|
||||
assert isinstance(key, dict)
|
||||
assert len(key) == 1
|
||||
|
||||
path = first(key)
|
||||
extra = key[path]
|
||||
|
||||
if not typ:
|
||||
d.append({BaseOutput.PARAM_PATH: path, **extra})
|
||||
continue
|
||||
|
||||
entry = {BaseOutput.PARAM_PATH: path}
|
||||
|
||||
persist = extra.pop(BaseOutput.PARAM_PERSIST, False)
|
||||
if persist:
|
||||
entry[BaseOutput.PARAM_PERSIST] = persist
|
||||
|
||||
cache = extra.pop(BaseOutput.PARAM_CACHE, True)
|
||||
if not cache:
|
||||
entry[BaseOutput.PARAM_CACHE] = cache
|
||||
|
||||
entry[typ] = extra or True
|
||||
|
||||
d.append(entry)
|
||||
|
||||
stage.outs.extend(output.loadd_from(stage, d))
|
||||
|
||||
@classmethod
|
||||
@@ -152,6 +176,8 @@ class StageLoader(Mapping):
|
||||
stage.deps, stage.outs = [], []
|
||||
|
||||
cls._load_outs(stage, stage_data.get("outs", []))
|
||||
cls._load_outs(stage, stage_data.get("metrics", []), "metric")
|
||||
cls._load_outs(stage, stage_data.get("plots", []), "plot")
|
||||
cls._load_deps(stage, stage_data.get("deps", []))
|
||||
cls._load_params(stage, stage_data.get("params", []))
|
||||
|
||||
|
||||
+2
-12
@@ -1,6 +1,3 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class StageParams:
|
||||
PARAM_MD5 = "md5"
|
||||
PARAM_CMD = "cmd"
|
||||
@@ -11,12 +8,5 @@ class StageParams:
|
||||
PARAM_META = "meta"
|
||||
PARAM_ALWAYS_CHANGED = "always_changed"
|
||||
PARAM_PARAMS = "params"
|
||||
|
||||
|
||||
class OutputParams(Enum):
|
||||
PERSIST = "outs_persist"
|
||||
PERSIST_NO_CACHE = "outs_persist_no_cache"
|
||||
METRICS_NO_CACHE = "metrics_no_cache"
|
||||
METRICS = "metrics"
|
||||
NO_CACHE = "outs_no_cache"
|
||||
OUTS = "outs"
|
||||
PARAM_METRICS = "metrics"
|
||||
PARAM_PLOTS = "plots"
|
||||
|
||||
+13
-2
@@ -13,7 +13,6 @@ from .exceptions import (
|
||||
StagePathNotFoundError,
|
||||
StagePathOutsideError,
|
||||
)
|
||||
from .params import OutputParams
|
||||
|
||||
|
||||
def check_stage_path(repo, path, is_wdir=False):
|
||||
@@ -39,14 +38,26 @@ def check_stage_path(repo, path, is_wdir=False):
|
||||
def fill_stage_outputs(stage, **kwargs):
|
||||
assert not stage.outs
|
||||
|
||||
keys = [
|
||||
"outs_persist",
|
||||
"outs_persist_no_cache",
|
||||
"metrics_no_cache",
|
||||
"metrics",
|
||||
"plots_no_cache",
|
||||
"plots",
|
||||
"outs_no_cache",
|
||||
"outs",
|
||||
]
|
||||
|
||||
stage.outs = []
|
||||
for key in (p.value for p in OutputParams):
|
||||
for key in keys:
|
||||
stage.outs += output.loads_from(
|
||||
stage,
|
||||
kwargs.get(key, []),
|
||||
use_cache="no_cache" not in key,
|
||||
persist="persist" in key,
|
||||
metric="metrics" in key,
|
||||
plot="plots" in key,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ select=B,C,E,F,W,T4,B9
|
||||
[isort]
|
||||
include_trailing_comma=true
|
||||
known_first_party=dvc,tests
|
||||
known_third_party=PyInstaller,RangeHTTPServer,boto3,bs4,colorama,configobj,distro,dpath,flaky,flufl,funcy,git,google,grandalf,mock,mockssh,moto,nanotime,packaging,paramiko,pathspec,pytest,requests,ruamel,setuptools,shortuuid,tqdm,voluptuous,yaml,zc
|
||||
known_third_party=PyInstaller,RangeHTTPServer,boto3,colorama,configobj,distro,dpath,flaky,flufl,funcy,git,google,grandalf,mock,mockssh,moto,nanotime,packaging,paramiko,pathspec,pytest,requests,ruamel,setuptools,shortuuid,tqdm,voluptuous,yaml,zc
|
||||
line_length=79
|
||||
multi_line_output=3
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import json
|
||||
|
||||
from .test_plots import PlotData, _run_with, _write_json
|
||||
|
||||
|
||||
def test_diff_dirty(tmp_dir, scm, dvc):
|
||||
metric_1 = [{"y": 2}, {"y": 3}]
|
||||
_write_json(tmp_dir, metric_1, "metric.json")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="init")
|
||||
|
||||
metric_2 = [{"y": 3}, {"y": 5}]
|
||||
_write_json(tmp_dir, metric_2, "metric.json")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="second")
|
||||
|
||||
metric_3 = [{"y": 5}, {"y": 6}]
|
||||
_write_json(tmp_dir, metric_3, "metric.json")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"])
|
||||
|
||||
plot_string = dvc.plots.diff(fields={"y"})["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"y": 5, PlotData.INDEX_FIELD: 0, "rev": "working tree"},
|
||||
{"y": 6, PlotData.INDEX_FIELD: 1, "rev": "working tree"},
|
||||
{"y": 3, PlotData.INDEX_FIELD: 0, "rev": "HEAD"},
|
||||
{"y": 5, PlotData.INDEX_FIELD: 1, "rev": "HEAD"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == PlotData.INDEX_FIELD
|
||||
assert plot_content["encoding"]["y"]["field"] == "y"
|
||||
+100
-119
@@ -7,15 +7,14 @@ from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from bs4 import BeautifulSoup
|
||||
from funcy import first
|
||||
|
||||
from dvc.repo.plots import NoDataOrTemplateProvided
|
||||
from dvc.repo.plots.data import (
|
||||
NoMetricInHistoryError,
|
||||
PlotData,
|
||||
PlotMetricTypeError,
|
||||
)
|
||||
from dvc.repo.plots.show import NoDataOrTemplateProvided
|
||||
from dvc.repo.plots.template import (
|
||||
NoDataForTemplateError,
|
||||
NoFieldInDataError,
|
||||
@@ -27,10 +26,14 @@ def _remove_whitespace(value):
|
||||
return value.replace(" ", "").replace("\n", "")
|
||||
|
||||
|
||||
def _run_with_metric(tmp_dir, metric_filename, commit=None, tag=None):
|
||||
tmp_dir.dvc.run(metrics_no_cache=[metric_filename], single_stage=True)
|
||||
def _run_with(tmp_dir, commit=None, tag=None, **kwargs):
|
||||
stage = tmp_dir.dvc.run(single_stage=True, **kwargs)
|
||||
if hasattr(tmp_dir.dvc, "scm"):
|
||||
tmp_dir.dvc.scm.add([metric_filename, metric_filename + ".dvc"])
|
||||
files = [stage.path]
|
||||
files += [
|
||||
os.fspath(out.path_info) for out in stage.outs if not out.use_cache
|
||||
]
|
||||
tmp_dir.dvc.scm.add(files)
|
||||
if commit:
|
||||
tmp_dir.dvc.scm.commit(commit)
|
||||
if tag:
|
||||
@@ -60,21 +63,20 @@ def test_plot_csv_one_column(tmp_dir, scm, dvc):
|
||||
# no header
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
_write_csv(metric, "metric.csv", header=False)
|
||||
_run_with_metric(tmp_dir, metric_filename="metric.csv")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.csv"])
|
||||
|
||||
plot_string = dvc.plot(
|
||||
"metric.csv",
|
||||
plot_string = dvc.plots.show(
|
||||
csv_header=False,
|
||||
x_title="x_title",
|
||||
y_title="y_title",
|
||||
title="mytitle",
|
||||
)
|
||||
)["metric.csv"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["title"] == "mytitle"
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"0": "2", PlotData.INDEX_FIELD: 0, "rev": "workspace"},
|
||||
{"0": "3", PlotData.INDEX_FIELD: 1, "rev": "workspace"},
|
||||
{"0": "2", PlotData.INDEX_FIELD: 0, "rev": "working tree"},
|
||||
{"0": "3", PlotData.INDEX_FIELD: 1, "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == PlotData.INDEX_FIELD
|
||||
assert plot_content["encoding"]["y"]["field"] == "0"
|
||||
@@ -88,23 +90,23 @@ def test_plot_csv_multiple_columns(tmp_dir, scm, dvc):
|
||||
OrderedDict([("first_val", 200), ("second_val", 300), ("val", 3)]),
|
||||
]
|
||||
_write_csv(metric, "metric.csv")
|
||||
_run_with_metric(tmp_dir, metric_filename="metric.csv")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.csv"])
|
||||
|
||||
plot_string = dvc.plot("metric.csv")
|
||||
plot_string = dvc.plots.show()["metric.csv"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{
|
||||
"val": "2",
|
||||
PlotData.INDEX_FIELD: 0,
|
||||
"rev": "workspace",
|
||||
"rev": "working tree",
|
||||
"first_val": "100",
|
||||
"second_val": "100",
|
||||
},
|
||||
{
|
||||
"val": "3",
|
||||
PlotData.INDEX_FIELD: 1,
|
||||
"rev": "workspace",
|
||||
"rev": "working tree",
|
||||
"first_val": "200",
|
||||
"second_val": "300",
|
||||
},
|
||||
@@ -119,23 +121,23 @@ def test_plot_csv_choose_axes(tmp_dir, scm, dvc):
|
||||
OrderedDict([("first_val", 200), ("second_val", 300), ("val", 3)]),
|
||||
]
|
||||
_write_csv(metric, "metric.csv")
|
||||
_run_with_metric(tmp_dir, metric_filename="metric.csv")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.csv"])
|
||||
|
||||
plot_string = dvc.plot(
|
||||
"metric.csv", x_field="first_val", y_field="second_val"
|
||||
)
|
||||
plot_string = dvc.plots.show(x_field="first_val", y_field="second_val")[
|
||||
"metric.csv"
|
||||
]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{
|
||||
"val": "2",
|
||||
"rev": "workspace",
|
||||
"rev": "working tree",
|
||||
"first_val": "100",
|
||||
"second_val": "100",
|
||||
},
|
||||
{
|
||||
"val": "3",
|
||||
"rev": "workspace",
|
||||
"rev": "working tree",
|
||||
"first_val": "200",
|
||||
"second_val": "300",
|
||||
},
|
||||
@@ -147,14 +149,14 @@ def test_plot_csv_choose_axes(tmp_dir, scm, dvc):
|
||||
def test_plot_json_single_val(tmp_dir, scm, dvc):
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "first run")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="first run")
|
||||
|
||||
plot_string = dvc.plot("metric.json")
|
||||
plot_string = dvc.plots.show()["metric.json"]
|
||||
|
||||
plot_json = json.loads(plot_string)
|
||||
assert plot_json["data"]["values"] == [
|
||||
{"val": 2, PlotData.INDEX_FIELD: 0, "rev": "workspace"},
|
||||
{"val": 3, PlotData.INDEX_FIELD: 1, "rev": "workspace"},
|
||||
{"val": 2, PlotData.INDEX_FIELD: 0, "rev": "working tree"},
|
||||
{"val": 3, PlotData.INDEX_FIELD: 1, "rev": "working tree"},
|
||||
]
|
||||
assert plot_json["encoding"]["x"]["field"] == PlotData.INDEX_FIELD
|
||||
assert plot_json["encoding"]["y"]["field"] == "val"
|
||||
@@ -166,9 +168,9 @@ def test_plot_json_multiple_val(tmp_dir, scm, dvc):
|
||||
{"first_val": 200, "val": 3},
|
||||
]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "first run")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="first run")
|
||||
|
||||
plot_string = dvc.plot("metric.json")
|
||||
plot_string = dvc.plots.show()["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -176,13 +178,13 @@ def test_plot_json_multiple_val(tmp_dir, scm, dvc):
|
||||
"val": 2,
|
||||
PlotData.INDEX_FIELD: 0,
|
||||
"first_val": 100,
|
||||
"rev": "workspace",
|
||||
"rev": "working tree",
|
||||
},
|
||||
{
|
||||
"val": 3,
|
||||
PlotData.INDEX_FIELD: 1,
|
||||
"first_val": 200,
|
||||
"rev": "workspace",
|
||||
"rev": "working tree",
|
||||
},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == PlotData.INDEX_FIELD
|
||||
@@ -195,19 +197,16 @@ def test_plot_confusion(tmp_dir, dvc):
|
||||
{"predicted": "A", "actual": "A"},
|
||||
]
|
||||
_write_json(tmp_dir, confusion_matrix, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "first run")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="first run")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
datafile="metric.json",
|
||||
template="confusion",
|
||||
x_field="predicted",
|
||||
y_field="actual",
|
||||
)
|
||||
plot_string = dvc.plots.show(
|
||||
template="confusion", x_field="predicted", y_field="actual",
|
||||
)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"predicted": "B", "actual": "A", "rev": "workspace"},
|
||||
{"predicted": "A", "actual": "A", "rev": "workspace"},
|
||||
{"predicted": "B", "actual": "A", "rev": "working tree"},
|
||||
{"predicted": "A", "actual": "A", "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == "predicted"
|
||||
assert plot_content["encoding"]["y"]["field"] == "actual"
|
||||
@@ -216,19 +215,21 @@ def test_plot_confusion(tmp_dir, dvc):
|
||||
def test_plot_multiple_revs_default(tmp_dir, scm, dvc):
|
||||
metric_1 = [{"y": 2}, {"y": 3}]
|
||||
_write_json(tmp_dir, metric_1, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "init", "v1")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="init", tag="v1")
|
||||
|
||||
metric_2 = [{"y": 3}, {"y": 5}]
|
||||
_write_json(tmp_dir, metric_2, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "second", "v2")
|
||||
_run_with(
|
||||
tmp_dir, plots_no_cache=["metric.json"], commit="second", tag="v2"
|
||||
)
|
||||
|
||||
metric_3 = [{"y": 5}, {"y": 6}]
|
||||
_write_json(tmp_dir, metric_3, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "third")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="third")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
"metric.json", fields={"y"}, revisions=["HEAD", "v2", "v1"],
|
||||
)
|
||||
plot_string = dvc.plots.show(fields={"y"}, revs=["HEAD", "v2", "v1"],)[
|
||||
"metric.json"
|
||||
]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -248,21 +249,21 @@ def test_plot_multiple_revs(tmp_dir, scm, dvc):
|
||||
|
||||
metric_1 = [{"y": 2}, {"y": 3}]
|
||||
_write_json(tmp_dir, metric_1, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "init", "v1")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="init", tag="v1")
|
||||
|
||||
metric_2 = [{"y": 3}, {"y": 5}]
|
||||
_write_json(tmp_dir, metric_2, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "second", "v2")
|
||||
_run_with(
|
||||
tmp_dir, plots_no_cache=["metric.json"], commit="second", tag="v2"
|
||||
)
|
||||
|
||||
metric_3 = [{"y": 5}, {"y": 6}]
|
||||
_write_json(tmp_dir, metric_3, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "third")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="third")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
"metric.json",
|
||||
template="template.json",
|
||||
revisions=["HEAD", "v2", "v1"],
|
||||
)
|
||||
plot_string = dvc.plots.show(
|
||||
template="template.json", revs=["HEAD", "v2", "v1"],
|
||||
)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
@@ -283,11 +284,16 @@ def test_plot_even_if_metric_missing(tmp_dir, scm, dvc, caplog):
|
||||
|
||||
metric = [{"y": 2}, {"y": 3}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "there is metric", "v2")
|
||||
_run_with(
|
||||
tmp_dir,
|
||||
plots_no_cache=["metric.json"],
|
||||
commit="there is metric",
|
||||
tag="v2",
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
with caplog.at_level(logging.WARNING, "dvc"):
|
||||
plot_string = dvc.plot("metric.json", revisions=["v1", "v2"])
|
||||
plot_string = dvc.plots.show(revs=["v1", "v2"])["metric.json"]
|
||||
assert (
|
||||
"File 'metric.json' was not found at: 'v1'. "
|
||||
"It will not be plotted." in caplog.text
|
||||
@@ -312,7 +318,7 @@ def test_throw_on_no_metric_at_all(tmp_dir, scm, dvc, caplog):
|
||||
with pytest.raises(NoMetricInHistoryError) as error, caplog.at_level(
|
||||
logging.WARNING, "dvc"
|
||||
):
|
||||
dvc.plot("metric.json", revisions=["v1"])
|
||||
dvc.plots.show(targets="metric.json", revs=["v1"])
|
||||
|
||||
# do not warn if none found
|
||||
assert len(caplog.messages) == 0
|
||||
@@ -332,16 +338,16 @@ def custom_template(tmp_dir, dvc):
|
||||
def test_custom_template(tmp_dir, scm, dvc, custom_template):
|
||||
metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "init", "v1")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="init", tag="v1")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
"metric.json", os.fspath(custom_template), x_field="a", y_field="b"
|
||||
)
|
||||
plot_string = dvc.plots.show(
|
||||
template=os.fspath(custom_template), x_field="a", y_field="b"
|
||||
)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"a": 1, "b": 2, "rev": "workspace"},
|
||||
{"a": 2, "b": 3, "rev": "workspace"},
|
||||
{"a": 1, "b": 2, "rev": "working tree"},
|
||||
{"a": 2, "b": 3, "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == "a"
|
||||
assert plot_content["encoding"]["y"]["field"] == "b"
|
||||
@@ -360,19 +366,16 @@ def test_custom_template_with_specified_data(
|
||||
|
||||
metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "init", "v1")
|
||||
_run_with(tmp_dir, outs_no_cache=["metric.json"], commit="init", tag="v1")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
datafile=None,
|
||||
template=os.fspath(custom_template),
|
||||
x_field="a",
|
||||
y_field="b",
|
||||
)
|
||||
plot_string = dvc.plots.show(
|
||||
template=os.fspath(custom_template), x_field="a", y_field="b",
|
||||
)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"a": 1, "b": 2, "rev": "workspace"},
|
||||
{"a": 2, "b": 3, "rev": "workspace"},
|
||||
{"a": 1, "b": 2, "rev": "working tree"},
|
||||
{"a": 2, "b": 3, "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == "a"
|
||||
assert plot_content["encoding"]["y"]["field"] == "b"
|
||||
@@ -391,16 +394,16 @@ def test_plot_override_specified_data_source(tmp_dir, scm, dvc):
|
||||
|
||||
metric = [{"a": 1, "b": 2}, {"a": 2, "b": 3}]
|
||||
_write_json(tmp_dir, metric, "metric2.json")
|
||||
_run_with_metric(tmp_dir, "metric2.json", "init", "v1")
|
||||
_run_with(tmp_dir, outs_no_cache=["metric2.json"], commit="init", tag="v1")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
datafile="metric2.json", template="newtemplate.json", x_field="a"
|
||||
)
|
||||
plot_string = dvc.plots.show(
|
||||
targets=["metric2.json"], template="newtemplate.json", x_field="a"
|
||||
)["metric2.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"a": 1, "b": 2, "rev": "workspace"},
|
||||
{"a": 2, "b": 3, "rev": "workspace"},
|
||||
{"a": 1, "b": 2, "rev": "working tree"},
|
||||
{"a": 2, "b": 3, "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == "a"
|
||||
assert plot_content["encoding"]["y"]["field"] == "b"
|
||||
@@ -408,42 +411,41 @@ def test_plot_override_specified_data_source(tmp_dir, scm, dvc):
|
||||
|
||||
def test_should_raise_on_no_template_and_datafile(tmp_dir, dvc):
|
||||
with pytest.raises(NoDataOrTemplateProvided):
|
||||
dvc.plot()
|
||||
dvc.plots.show()
|
||||
|
||||
|
||||
def test_should_raise_on_no_template(tmp_dir, dvc):
|
||||
with pytest.raises(TemplateNotFoundError):
|
||||
dvc.plot("metric.json", "non_existing_template.json")
|
||||
dvc.plots.show("metric.json", template="non_existing_template.json")
|
||||
|
||||
|
||||
def test_plot_no_data(tmp_dir, dvc):
|
||||
with pytest.raises(NoDataForTemplateError):
|
||||
dvc.plot(template="default")
|
||||
dvc.plots.show(template="default")
|
||||
|
||||
|
||||
def test_plot_wrong_metric_type(tmp_dir, scm, dvc):
|
||||
tmp_dir.scm_gen("metric.txt", "content", commit="initial")
|
||||
with pytest.raises(PlotMetricTypeError):
|
||||
dvc.plot(datafile="metric.txt")
|
||||
dvc.plots.show(targets=["metric.txt"])
|
||||
|
||||
|
||||
def test_plot_choose_columns(tmp_dir, scm, dvc, custom_template):
|
||||
metric = [{"a": 1, "b": 2, "c": 3}, {"a": 2, "b": 3, "c": 4}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "init", "v1")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="init", tag="v1")
|
||||
|
||||
plot_string = dvc.plot(
|
||||
"metric.json",
|
||||
os.fspath(custom_template),
|
||||
plot_string = dvc.plots.show(
|
||||
template=os.fspath(custom_template),
|
||||
fields={"b", "c"},
|
||||
x_field="b",
|
||||
y_field="c",
|
||||
)
|
||||
)["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"b": 2, "c": 3, "rev": "workspace"},
|
||||
{"b": 3, "c": 4, "rev": "workspace"},
|
||||
{"b": 2, "c": 3, "rev": "working tree"},
|
||||
{"b": 3, "c": 4, "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == "b"
|
||||
assert plot_content["encoding"]["y"]["field"] == "c"
|
||||
@@ -452,63 +454,42 @@ def test_plot_choose_columns(tmp_dir, scm, dvc, custom_template):
|
||||
def test_plot_default_choose_column(tmp_dir, scm, dvc):
|
||||
metric = [{"a": 1, "b": 2, "c": 3}, {"a": 2, "b": 3, "c": 4}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "init", "v1")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="init", tag="v1")
|
||||
|
||||
plot_string = dvc.plot("metric.json", fields={"b"})
|
||||
plot_string = dvc.plots.show(fields={"b"})["metric.json"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{PlotData.INDEX_FIELD: 0, "b": 2, "rev": "workspace"},
|
||||
{PlotData.INDEX_FIELD: 1, "b": 3, "rev": "workspace"},
|
||||
{PlotData.INDEX_FIELD: 0, "b": 2, "rev": "working tree"},
|
||||
{PlotData.INDEX_FIELD: 1, "b": 3, "rev": "working tree"},
|
||||
]
|
||||
assert plot_content["encoding"]["x"]["field"] == PlotData.INDEX_FIELD
|
||||
assert plot_content["encoding"]["y"]["field"] == "b"
|
||||
|
||||
|
||||
def test_plot_embed(tmp_dir, scm, dvc):
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "first run")
|
||||
|
||||
plot_string = dvc.plot("metric.json", embed=True, y_field="val")
|
||||
|
||||
page_content = BeautifulSoup(plot_string)
|
||||
data_dump = json.dumps(
|
||||
[
|
||||
{"val": 2, PlotData.INDEX_FIELD: 0, "rev": "workspace"},
|
||||
{"val": 3, PlotData.INDEX_FIELD: 1, "rev": "workspace"},
|
||||
],
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
assert _remove_whitespace(data_dump) in _remove_whitespace(
|
||||
first(page_content.body.script.contents)
|
||||
)
|
||||
|
||||
|
||||
def test_plot_yaml(tmp_dir, scm, dvc):
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
with open("metric.yaml", "w") as fobj:
|
||||
yaml.dump(metric, fobj)
|
||||
|
||||
_run_with_metric(tmp_dir, metric_filename="metric.yaml")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.yaml"])
|
||||
|
||||
plot_string = dvc.plot("metric.yaml",)
|
||||
plot_string = dvc.plots.show()["metric.yaml"]
|
||||
|
||||
plot_content = json.loads(plot_string)
|
||||
assert plot_content["data"]["values"] == [
|
||||
{"val": 2, PlotData.INDEX_FIELD: 0, "rev": "workspace"},
|
||||
{"val": 3, PlotData.INDEX_FIELD: 1, "rev": "workspace"},
|
||||
{"val": 2, PlotData.INDEX_FIELD: 0, "rev": "working tree"},
|
||||
{"val": 3, PlotData.INDEX_FIELD: 1, "rev": "working tree"},
|
||||
]
|
||||
|
||||
|
||||
def test_raise_on_wrong_field(tmp_dir, scm, dvc):
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
_write_json(tmp_dir, metric, "metric.json")
|
||||
_run_with_metric(tmp_dir, "metric.json", "first run")
|
||||
_run_with(tmp_dir, plots_no_cache=["metric.json"], commit="first run")
|
||||
|
||||
with pytest.raises(NoFieldInDataError):
|
||||
dvc.plot("metric.json", x_field="no_val")
|
||||
dvc.plots.show("metric.json", x_field="no_val")
|
||||
|
||||
with pytest.raises(NoFieldInDataError):
|
||||
dvc.plot("metric.json", y_field="no_val")
|
||||
dvc.plots.show("metric.json", y_field="no_val")
|
||||
|
||||
@@ -167,11 +167,8 @@ def test_run_dump_on_multistage(tmp_dir, dvc, run_head):
|
||||
),
|
||||
"wdir": "dir",
|
||||
"deps": ["bar", "foo", "foobar"],
|
||||
"outs": [
|
||||
"bar-1",
|
||||
{"foo-1": {"persist": True}},
|
||||
{"foobar-1": {"metric": True, "cache": False}},
|
||||
],
|
||||
"outs": ["bar-1", {"foo-1": {"persist": True}}],
|
||||
"metrics": [{"foobar-1": {"cache": False}}],
|
||||
},
|
||||
**data["stages"],
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from dvc.cli import parse_args
|
||||
from dvc.command.plots import CmdPlotsDiff, CmdPlotsShow
|
||||
|
||||
|
||||
def test_metrics_diff(mocker):
|
||||
def test_metrics_diff(dvc, mocker):
|
||||
cli_args = parse_args(
|
||||
[
|
||||
"plots",
|
||||
"diff",
|
||||
"--file",
|
||||
"--out",
|
||||
"result.extension",
|
||||
"-t",
|
||||
"template",
|
||||
"-d",
|
||||
"--targets",
|
||||
"datafile",
|
||||
"--select",
|
||||
"column1,column2",
|
||||
"--no-html",
|
||||
"--stdout",
|
||||
"--show-json",
|
||||
"-x",
|
||||
"x_field",
|
||||
"-y",
|
||||
@@ -37,20 +34,17 @@ def test_metrics_diff(mocker):
|
||||
assert cli_args.func == CmdPlotsDiff
|
||||
|
||||
cmd = cli_args.func(cli_args)
|
||||
|
||||
m = mocker.patch.object(cmd.repo, "plot", autospec=True)
|
||||
mocker.patch("builtins.open")
|
||||
mocker.patch("os.path.join")
|
||||
m = mocker.patch("dvc.repo.plots.diff.diff", return_value={})
|
||||
|
||||
assert cmd.run() == 0
|
||||
|
||||
m.assert_called_once_with(
|
||||
datafile="datafile",
|
||||
cmd.repo,
|
||||
targets=["datafile"],
|
||||
template="template",
|
||||
revisions=["HEAD", "tag1", "tag2"],
|
||||
revs=["HEAD", "tag1", "tag2"],
|
||||
fields={"column1", "column2"},
|
||||
path=None,
|
||||
embed=False,
|
||||
x_field="x_field",
|
||||
y_field="y_field",
|
||||
csv_header=True,
|
||||
@@ -60,19 +54,18 @@ def test_metrics_diff(mocker):
|
||||
)
|
||||
|
||||
|
||||
def test_metrics_show(mocker):
|
||||
def test_metrics_show(dvc, mocker):
|
||||
cli_args = parse_args(
|
||||
[
|
||||
"plots",
|
||||
"show",
|
||||
"-f",
|
||||
"-o",
|
||||
"result.extension",
|
||||
"-t",
|
||||
"template",
|
||||
"-s",
|
||||
"$.data",
|
||||
"--no-html",
|
||||
"--stdout",
|
||||
"--show-json",
|
||||
"--no-csv-header",
|
||||
"datafile",
|
||||
]
|
||||
@@ -81,19 +74,16 @@ def test_metrics_show(mocker):
|
||||
|
||||
cmd = cli_args.func(cli_args)
|
||||
|
||||
m = mocker.patch.object(cmd.repo, "plot", autospec=True)
|
||||
mocker.patch("builtins.open")
|
||||
mocker.patch("os.path.join")
|
||||
m = mocker.patch("dvc.repo.plots.show.show", return_value={})
|
||||
|
||||
assert cmd.run() == 0
|
||||
|
||||
m.assert_called_once_with(
|
||||
datafile="datafile",
|
||||
cmd.repo,
|
||||
targets=["datafile"],
|
||||
template="template",
|
||||
revisions=None,
|
||||
fields=None,
|
||||
path="$.data",
|
||||
embed=False,
|
||||
x_field=None,
|
||||
y_field=None,
|
||||
csv_header=False,
|
||||
@@ -103,20 +93,13 @@ def test_metrics_show(mocker):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arg_revisions,is_dirty,expected_revisions",
|
||||
[
|
||||
([], False, ["workspace"]),
|
||||
([], True, ["HEAD", "workspace"]),
|
||||
(["v1", "v2", "workspace"], False, ["v1", "v2", "workspace"]),
|
||||
(["v1", "v2", "workspace"], True, ["v1", "v2", "workspace"]),
|
||||
],
|
||||
)
|
||||
def test_revisions(mocker, arg_revisions, is_dirty, expected_revisions):
|
||||
args = mocker.MagicMock()
|
||||
|
||||
cmd = CmdPlotsDiff(args)
|
||||
mocker.patch.object(args, "revisions", arg_revisions)
|
||||
mocker.patch.object(cmd.repo.scm, "is_dirty", return_value=is_dirty)
|
||||
|
||||
assert cmd._revisions() == expected_revisions
|
||||
def test_plots_show_json(dvc, mocker, caplog):
|
||||
cli_args = parse_args(
|
||||
["plots", "diff", "HEAD~10", "HEAD~1", "--show-json"]
|
||||
)
|
||||
cmd = cli_args.func(cli_args)
|
||||
mocker.patch(
|
||||
"dvc.repo.plots.diff.diff", return_value={"plots.csv": "plothtml"}
|
||||
)
|
||||
assert cmd.run() == 0
|
||||
assert '{"plots.csv": "plothtml"}\n' in caplog.text
|
||||
|
||||
@@ -18,6 +18,10 @@ def test_run(mocker, dvc):
|
||||
"metrics",
|
||||
"--metrics-no-cache",
|
||||
"metrics-no-cache",
|
||||
"--plots",
|
||||
"plots",
|
||||
"--plots-no-cache",
|
||||
"plots-no-cache",
|
||||
"--file",
|
||||
"file",
|
||||
"--wdir",
|
||||
@@ -51,6 +55,8 @@ def test_run(mocker, dvc):
|
||||
outs_no_cache=["outs-no-cache"],
|
||||
metrics=["metrics"],
|
||||
metrics_no_cache=["metrics-no-cache"],
|
||||
plots=["plots"],
|
||||
plots_no_cache=["plots-no-cache"],
|
||||
outs_persist=["outs-persist"],
|
||||
outs_persist_no_cache=["outs-persist-no-cache"],
|
||||
params=["file:param1,param2", "param3"],
|
||||
@@ -78,6 +84,8 @@ def test_run_args_from_cli(mocker, dvc):
|
||||
outs_no_cache=[],
|
||||
metrics=[],
|
||||
metrics_no_cache=[],
|
||||
plots=[],
|
||||
plots_no_cache=[],
|
||||
outs_persist=[],
|
||||
outs_persist_no_cache=[],
|
||||
params=[],
|
||||
@@ -105,6 +113,8 @@ def test_run_args_with_spaces(mocker, dvc):
|
||||
outs_no_cache=[],
|
||||
metrics=[],
|
||||
metrics_no_cache=[],
|
||||
plots=[],
|
||||
plots_no_cache=[],
|
||||
outs_persist=[],
|
||||
outs_persist_no_cache=[],
|
||||
params=[],
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from dvc.repo.plots.diff import _revisions
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arg_revisions,is_dirty,expected_revisions",
|
||||
[
|
||||
([], False, ["working tree"]),
|
||||
([], True, ["HEAD", "working tree"]),
|
||||
(["v1", "v2", "working tree"], False, ["v1", "v2", "working tree"]),
|
||||
(["v1", "v2", "working tree"], True, ["v1", "v2", "working tree"]),
|
||||
],
|
||||
)
|
||||
def test_revisions(mocker, arg_revisions, is_dirty, expected_revisions):
|
||||
assert _revisions(arg_revisions, is_dirty) == expected_revisions
|
||||
Reference in New Issue
Block a user