diff --git a/dvc/command/plots.py b/dvc/command/plots.py
index 07761eef4..2de075cd1 100644
--- a/dvc/command/plots.py
+++ b/dvc/command/plots.py
@@ -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 = """
+
+
+ DVC Plot
+
+
+
+
+
+ {divs}
+
+"""
+
+DIV_HTML = """
+"""
+
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(
diff --git a/dvc/command/run.py b/dvc/command/run.py
index a7a495091..77f653830 100644
--- a/dvc/command/run.py
+++ b/dvc/command/run.py
@@ -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="",
)
+ run_parser.add_argument(
+ "--plots",
+ action="append",
+ default=[],
+ help="Declare output plot file.",
+ metavar="",
+ )
+ run_parser.add_argument(
+ "--plots-no-cache",
+ action="append",
+ default=[],
+ help="Declare output plot file (do not put into DVC cache).",
+ metavar="",
+ )
run_parser.add_argument(
"-f",
"--file",
diff --git a/dvc/dvcfile.py b/dvc/dvcfile.py
index 513ec76d4..c2722c7a7 100644
--- a/dvc/dvcfile.py
+++ b/dvc/dvcfile.py
@@ -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):
diff --git a/dvc/output/__init__.py b/dvc/output/__init__.py
index 43d2d2a28..492b0e486 100644
--- a/dvc/output/__init__.py
+++ b/dvc/output/__init__.py
@@ -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
]
diff --git a/dvc/output/base.py b/dvc/output/base.py
index ce00bd653..fc9b52fdb 100644
--- a/dvc/output/base.py
+++ b/dvc/output/base.py
@@ -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
diff --git a/dvc/output/local.py b/dvc/output/local.py
index 2ca840135..f84d3478b 100644
--- a/dvc/output/local.py
+++ b/dvc/output/local.py
@@ -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))
diff --git a/dvc/repo/__init__.py b/dvc/repo/__init__.py
index 6c4b56f74..213cbdcb4 100644
--- a/dvc/repo/__init__.py
+++ b/dvc/repo/__init__.py
@@ -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()
diff --git a/dvc/repo/brancher.py b/dvc/repo/brancher.py
index 67a3b19b3..0fbf2f403 100644
--- a/dvc/repo/brancher.py
+++ b/dvc/repo/brancher.py
@@ -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:
diff --git a/dvc/repo/plots/__init__.py b/dvc/repo/plots/__init__.py
index 11219d801..5f06b7491 100644
--- a/dvc/repo/plots/__init__.py
+++ b/dvc/repo/plots/__init__.py
@@ -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 = """
-
-
- DVC Plot
-
-
-
-
-
- {divs}
-
-"""
-
-DIV_HTML = """
-"""
-
-
-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)
diff --git a/dvc/repo/plots/data.py b/dvc/repo/plots/data.py
index 4994aea33..7fb811f1d 100644
--- a/dvc/repo/plots/data.py
+++ b/dvc/repo/plots/data.py
@@ -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:
diff --git a/dvc/repo/plots/diff.py b/dvc/repo/plots/diff.py
new file mode 100644
index 000000000..e03e47114
--- /dev/null
+++ b/dvc/repo/plots/diff.py
@@ -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
+ )
diff --git a/dvc/repo/plots/show.py b/dvc/repo/plots/show.py
new file mode 100644
index 000000000..225b1dc82
--- /dev/null
+++ b/dvc/repo/plots/show.py
@@ -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
diff --git a/dvc/repo/run.py b/dvc/repo/run.py
index b8be523fa..0b6f0354f 100644
--- a/dvc/repo/run.py
+++ b/dvc/repo/run.py
@@ -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", []),
)
diff --git a/dvc/schema.py b/dvc/schema.py
index 429e006af..affc63d75 100644
--- a/dvc/schema.py
+++ b/dvc/schema.py
@@ -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}
diff --git a/dvc/serialize.py b/dvc/serialize.py
index 00487f92f..b3648f596 100644
--- a/dvc/serialize.py
+++ b/dvc/serialize.py
@@ -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),
]
diff --git a/dvc/stage/exceptions.py b/dvc/stage/exceptions.py
index 6c267a832..4be365238 100644
--- a/dvc/stage/exceptions.py
+++ b/dvc/stage/exceptions.py
@@ -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):
diff --git a/dvc/stage/loader.py b/dvc/stage/loader.py
index 4f30a9d44..d973f31a9 100644
--- a/dvc/stage/loader.py
+++ b/dvc/stage/loader.py
@@ -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", []))
diff --git a/dvc/stage/params.py b/dvc/stage/params.py
index 9ae388636..a7885541e 100644
--- a/dvc/stage/params.py
+++ b/dvc/stage/params.py
@@ -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"
diff --git a/dvc/stage/utils.py b/dvc/stage/utils.py
index 38a68390c..6312c88da 100644
--- a/dvc/stage/utils.py
+++ b/dvc/stage/utils.py
@@ -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,
)
diff --git a/setup.cfg b/setup.cfg
index 0e5233000..723c7fa14 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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
diff --git a/tests/func/plots/test_diff.py b/tests/func/plots/test_diff.py
new file mode 100644
index 000000000..e488ec75f
--- /dev/null
+++ b/tests/func/plots/test_diff.py
@@ -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"
diff --git a/tests/func/plots/test_plots.py b/tests/func/plots/test_plots.py
index a8be76a83..e8923a437 100644
--- a/tests/func/plots/test_plots.py
+++ b/tests/func/plots/test_plots.py
@@ -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")
diff --git a/tests/func/test_run_multistage.py b/tests/func/test_run_multistage.py
index 078afa888..75cad0d8a 100644
--- a/tests/func/test_run_multistage.py
+++ b/tests/func/test_run_multistage.py
@@ -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"],
}
diff --git a/tests/unit/command/test_plots.py b/tests/unit/command/test_plots.py
index ac10e42b6..405492eff 100644
--- a/tests/unit/command/test_plots.py
+++ b/tests/unit/command/test_plots.py
@@ -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
diff --git a/tests/unit/command/test_run.py b/tests/unit/command/test_run.py
index 00cd61fdf..87695291a 100644
--- a/tests/unit/command/test_run.py
+++ b/tests/unit/command/test_run.py
@@ -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=[],
diff --git a/tests/unit/repo/plots/test_diff.py b/tests/unit/repo/plots/test_diff.py
new file mode 100644
index 000000000..2b16e71e4
--- /dev/null
+++ b/tests/unit/repo/plots/test_diff.py
@@ -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