mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 23:09:28 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2edbae7e3 | ||
|
|
be0c827b95 | ||
|
|
d6ecef7169 | ||
|
|
e8b649a75f | ||
|
|
8767f42f39 | ||
|
|
8580e66d60 | ||
|
|
c24ae2b0c3 | ||
|
|
4e17279440 | ||
|
|
50cdf4f213 | ||
|
|
f628f670c9 | ||
|
|
d7489bb65f | ||
|
|
5a4f4d3a8e | ||
|
|
2c24b24562 | ||
|
|
df9f76c2c0 | ||
|
|
0db3ece060 | ||
|
|
7c437cc349 | ||
|
|
3ed5efceb7 | ||
|
|
d33df60e41 | ||
|
|
c5415342d0 | ||
|
|
dd86076ba6 | ||
|
|
7dc3859400 | ||
|
|
4f3b28e268 | ||
|
|
32f435c47f | ||
|
|
054feae778 | ||
|
|
2abe515391 | ||
|
|
1eb89a2dc0 | ||
|
|
9c300a0a1c | ||
|
|
7b68a6f0ca | ||
|
|
ee823a59b9 | ||
|
|
16f404b16a | ||
|
|
0eda4bd91c | ||
|
|
a098a50c8d | ||
|
|
f57a378c0c | ||
|
|
3e0a50e52e | ||
|
|
ec72866eb9 | ||
|
|
e53d390ff9 | ||
|
|
dd330ac592 | ||
|
|
4ccb712b6d | ||
|
|
ecee2d3522 | ||
|
|
20bb6ddf3c | ||
|
|
e58fcd864b | ||
|
|
0514db3bfa | ||
|
|
1e0b2d7af0 | ||
|
|
bfcc8f877c | ||
|
|
46ad8745d3 | ||
|
|
eb13d3ca83 | ||
|
|
f46beb4958 | ||
|
|
9a1d74048a | ||
|
|
cce6aed378 | ||
|
|
612696cf4a | ||
|
|
3f5f983f5f | ||
|
|
fed9f088ca | ||
|
|
05eaeef012 | ||
|
|
48c0527755 | ||
|
|
4668a35f64 | ||
|
|
8107ec849b | ||
|
|
19284b3b4c | ||
|
|
315d1cde99 | ||
|
|
1d415efd0b | ||
|
|
e7c20aa1d8 | ||
|
|
058377f33e | ||
|
|
c7b1b14905 | ||
|
|
fbd8f5c185 | ||
|
|
7fe370b729 | ||
|
|
f9df6d2a16 | ||
|
|
2e4fb376da | ||
|
|
6d6a3f17b1 | ||
|
|
bdaf4aced6 | ||
|
|
0bf1802d4b | ||
|
|
6d3045320b | ||
|
|
3469dda8b8 | ||
|
|
eefede92b2 | ||
|
|
e295434ec6 | ||
|
|
b1f1b01ffc | ||
|
|
f41d7effdb | ||
|
|
6a417885b8 | ||
|
|
4221d9f70b | ||
|
|
5822d53687 | ||
|
|
a478a0ea98 | ||
|
|
e81c88e501 | ||
|
|
b286c311f4 |
@@ -0,0 +1,23 @@
|
||||
name: benchmarks
|
||||
on: [pull_request, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: run benchmarks
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-18.04]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
- uses: iterative/dvc-bench@master
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
|
||||
@@ -16,6 +16,10 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/[email protected]
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
@@ -38,17 +42,12 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-18.04, windows-2019, macos-10.15]
|
||||
pyv: ["3.6", "3.7", "3.8", "3.9"]
|
||||
include:
|
||||
- os: "macos-10.15"
|
||||
pyv: "3.10.0-rc.1"
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.6", "3.7", "3.8", "3.9", "3.10"]
|
||||
exclude:
|
||||
- os: macos-10.15
|
||||
pyv: "3.9"
|
||||
- os: macos-10.15
|
||||
pyv: "3.7"
|
||||
|
||||
# no wheels for pygit2 yet
|
||||
- os: windows-latest
|
||||
pyv: "3.10"
|
||||
steps:
|
||||
- uses: actions/[email protected]
|
||||
- name: Set up Python
|
||||
@@ -64,13 +63,9 @@ jobs:
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
path: ${{ steps.pip-cache-dir.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
- name: install libgit2 on Python3.10 in macOS
|
||||
if: matrix.pyv == '3.10.0-rc.1' && matrix.os == 'macos-10.15'
|
||||
run: |
|
||||
brew install libgit2
|
||||
- name: install
|
||||
if: steps.cache.pip-cache-dir.cache-hit != 'true'
|
||||
run: |
|
||||
@@ -99,7 +94,7 @@ jobs:
|
||||
run: |
|
||||
echo "extra_test_args=--disable-gdrive" >> $GITHUB_ENV
|
||||
- name: run tests
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
|
||||
@@ -6,7 +6,7 @@ repos:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 21.8b0
|
||||
rev: 21.9b0
|
||||
- hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
Copyright 2017-2021 Iterative, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
include fastentrypoints.py
|
||||
include LICENSE
|
||||
recursive-include requirements *.txt
|
||||
|
||||
+40
-1
@@ -1,10 +1,39 @@
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Callable, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from argparse import Namespace
|
||||
|
||||
|
||||
@contextmanager
|
||||
def yappi_profile(
|
||||
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
|
||||
):
|
||||
try:
|
||||
import yappi # pylint: disable=import-error
|
||||
except ImportError:
|
||||
print("Failed to run profiler, yappi is not installed")
|
||||
yield
|
||||
return
|
||||
|
||||
yappi.set_clock_type("wall" if wall_clock else "cpu")
|
||||
|
||||
yappi.start()
|
||||
yield
|
||||
yappi.stop()
|
||||
|
||||
# pylint:disable=no-member
|
||||
if path:
|
||||
stats = yappi.get_func_stats()
|
||||
fpath = path() if callable(path) else path
|
||||
stats.save(fpath, "callgrind")
|
||||
else:
|
||||
yappi.get_func_stats().print_all()
|
||||
yappi.get_thread_stats().print_all()
|
||||
|
||||
yappi.clear_stats()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def instrument(html_output=False):
|
||||
"""Run a statistical profiler"""
|
||||
@@ -70,6 +99,13 @@ def debugtools(args: "Namespace" = None, **kwargs):
|
||||
stack.enter_context(profile(kw.get("cprofile_dump")))
|
||||
if kw.get("instrument") or kw.get("instrument_open"):
|
||||
stack.enter_context(instrument(kw.get("instrument_open", False)))
|
||||
if kw.get("yappi"):
|
||||
from datetime import datetime
|
||||
|
||||
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
|
||||
stack.enter_context(
|
||||
yappi_profile(path=lambda: output.format(datetime.now()))
|
||||
)
|
||||
yield
|
||||
|
||||
|
||||
@@ -79,6 +115,9 @@ def add_debugging_flags(parser):
|
||||
parser.add_argument(
|
||||
"--cprofile", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"--yappi", action="store_true", default=False, help=SUPPRESS
|
||||
)
|
||||
parser.add_argument("--cprofile-dump", help=SUPPRESS)
|
||||
parser.add_argument(
|
||||
"--pdb", action="store_true", default=False, help=SUPPRESS
|
||||
|
||||
+2
-3
@@ -103,9 +103,8 @@ class CmdDAG(CmdBase):
|
||||
if self.args.dot:
|
||||
ui.write(_show_dot(G))
|
||||
else:
|
||||
from dvc.utils.pager import pager
|
||||
|
||||
pager(_show_ascii(G))
|
||||
with ui.pager():
|
||||
ui.write(_show_ascii(G))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -393,6 +393,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
help="Show status of all stages in the specified directory.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
|
||||
+8
-6
@@ -1,5 +1,4 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -16,7 +15,7 @@ def _digest(checksum):
|
||||
return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
|
||||
|
||||
|
||||
def _show_md(diff, show_hash=False, hide_missing=False):
|
||||
def _show_markdown(diff, show_hash=False, hide_missing=False):
|
||||
headers = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
|
||||
rows = []
|
||||
statuses = ["added", "deleted", "renamed", "modified"]
|
||||
@@ -142,10 +141,10 @@ class CmdDiff(CmdBase):
|
||||
del entry["hash"]
|
||||
diff[key] = entries
|
||||
|
||||
if self.args.show_json:
|
||||
ui.write(json.dumps(diff))
|
||||
elif self.args.show_md:
|
||||
_show_md(diff, show_hash, hide_missing)
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
elif self.args.markdown:
|
||||
_show_markdown(diff, show_hash, hide_missing)
|
||||
elif diff:
|
||||
self._show_diff(diff, hide_missing)
|
||||
|
||||
@@ -188,6 +187,7 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
help="Format the output into a JSON",
|
||||
action="store_true",
|
||||
@@ -200,9 +200,11 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
action="store_true",
|
||||
dest="markdown",
|
||||
default=False,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
|
||||
+107
-73
@@ -1,14 +1,12 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import date, datetime
|
||||
from fnmatch import fnmatch
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional
|
||||
|
||||
from funcy import lmap
|
||||
from funcy import compact, lmap
|
||||
|
||||
from dvc import prompt
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.command.metrics import DEFAULT_PRECISION
|
||||
@@ -20,9 +18,8 @@ from dvc.utils.flatten import flatten
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.compare import TabularData
|
||||
from dvc.ui import RichText
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -261,11 +258,12 @@ def _sort_column(sort_by, metric_names, param_names):
|
||||
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
|
||||
def _sort(item):
|
||||
rev, exp = item
|
||||
tip = exp.get("checkpoint_tip")
|
||||
exp_data = exp.get("data", {})
|
||||
tip = exp_data.get("checkpoint_tip")
|
||||
if tip and tip != rev:
|
||||
# Sort checkpoint experiments by tip commit
|
||||
return _sort((tip, experiments[tip]))
|
||||
data = exp.get(typ, {}).get(sort_path, {})
|
||||
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
|
||||
val = flatten(data).get(sort_name)
|
||||
return val is None, val
|
||||
|
||||
@@ -292,8 +290,6 @@ def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
|
||||
|
||||
|
||||
def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.compare import _format_field, with_value
|
||||
|
||||
if not items:
|
||||
@@ -308,10 +304,10 @@ def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
|
||||
item.get(name),
|
||||
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
|
||||
)
|
||||
# wrap field data in rich.Text, otherwise rich may
|
||||
# wrap field data in ui.rich_text, otherwise rich may
|
||||
# interpret unescaped braces from list/dict types as rich
|
||||
# markup tags
|
||||
row.append(Text(str(_format_field(value, precision))))
|
||||
row.append(ui.rich_text(str(_format_field(value, precision))))
|
||||
|
||||
|
||||
def _parse_filter_list(param_list):
|
||||
@@ -362,17 +358,15 @@ def experiments_table(
|
||||
return td
|
||||
|
||||
|
||||
def prepare_exp_id(kwargs) -> "Text":
|
||||
from rich.text import Text
|
||||
|
||||
def prepare_exp_id(kwargs) -> "RichText":
|
||||
exp_name = kwargs["Experiment"]
|
||||
rev = kwargs["rev"]
|
||||
typ = kwargs.get("typ", "baseline")
|
||||
|
||||
if typ == "baseline" or not exp_name:
|
||||
text = Text(exp_name or rev)
|
||||
text = ui.rich_text(exp_name or rev)
|
||||
else:
|
||||
text = Text.assemble(rev, " [", (exp_name, "bold"), "]")
|
||||
text = ui.rich_text.assemble(rev, " [", (exp_name, "bold"), "]")
|
||||
|
||||
parent = kwargs.get("parent")
|
||||
suff = f" ({parent})" if parent else ""
|
||||
@@ -380,7 +374,7 @@ def prepare_exp_id(kwargs) -> "Text":
|
||||
|
||||
tree = experiment_types[typ]
|
||||
pref = f"{tree} " if tree else ""
|
||||
return Text(pref) + text
|
||||
return ui.rich_text(pref) + text
|
||||
|
||||
|
||||
def baseline_styler(typ):
|
||||
@@ -388,7 +382,7 @@ def baseline_styler(typ):
|
||||
|
||||
|
||||
def show_experiments(
|
||||
all_experiments, pager=True, no_timestamp=False, show_csv=False, **kwargs
|
||||
all_experiments, pager=True, no_timestamp=False, csv=False, **kwargs
|
||||
):
|
||||
from funcy.seqs import flatten as flatten_list
|
||||
|
||||
@@ -444,7 +438,7 @@ def show_experiments(
|
||||
|
||||
row_styles = lmap(baseline_styler, td.column("typ"))
|
||||
|
||||
if not show_csv:
|
||||
if not csv:
|
||||
merge_headers = ["Experiment", "rev", "typ", "parent"]
|
||||
td.column("Experiment")[:] = map(
|
||||
prepare_exp_id, td.as_dict(merge_headers)
|
||||
@@ -478,7 +472,7 @@ def show_experiments(
|
||||
rich_table=True,
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
show_csv=show_csv,
|
||||
csv=csv,
|
||||
)
|
||||
|
||||
|
||||
@@ -511,18 +505,16 @@ class CmdExperimentsShow(CmdBase):
|
||||
logger.exception("failed to show experiments")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(all_experiments, default=_format_json))
|
||||
if self.args.json:
|
||||
ui.write_json(all_experiments, default=_format_json)
|
||||
else:
|
||||
precision = (
|
||||
self.args.precision or None
|
||||
if self.args.show_csv
|
||||
if self.args.csv
|
||||
else DEFAULT_PRECISION
|
||||
)
|
||||
fill_value = "" if self.args.show_csv else FILL_VALUE
|
||||
iso = True if self.args.show_csv else False
|
||||
fill_value = "" if self.args.csv else FILL_VALUE
|
||||
iso = True if self.args.csv else False
|
||||
|
||||
show_experiments(
|
||||
all_experiments,
|
||||
@@ -537,7 +529,7 @@ class CmdExperimentsShow(CmdBase):
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
pager=not self.args.no_pager,
|
||||
show_csv=self.args.show_csv,
|
||||
csv=self.args.csv,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -566,10 +558,8 @@ class CmdExperimentsDiff(CmdBase):
|
||||
logger.exception("failed to show experiments diff")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(diff))
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
@@ -584,7 +574,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
show_diff(
|
||||
diff[key],
|
||||
title=title,
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
old=self.args.old,
|
||||
on_empty_diff="diff not supported",
|
||||
@@ -669,7 +659,7 @@ class CmdExperimentsGC(CmdRepro):
|
||||
logger.warning(msg)
|
||||
|
||||
msg = "Are you sure you want to proceed?"
|
||||
if not self.args.force and not prompt.confirm(msg):
|
||||
if not self.args.force and not ui.confirm(msg):
|
||||
return 1
|
||||
|
||||
removed = self.repo.experiments.gc(
|
||||
@@ -798,54 +788,69 @@ class CmdExperimentsInit(CmdBase):
|
||||
DEFAULT_PARAMS = "params.yaml"
|
||||
PLOTS = "plots"
|
||||
DVCLIVE = "dvclive"
|
||||
DEFAULT_NAME = "default"
|
||||
DEFAULTS = {
|
||||
"code": CODE,
|
||||
"data": DATA,
|
||||
"models": MODELS,
|
||||
"metrics": DEFAULT_METRICS,
|
||||
"params": DEFAULT_PARAMS,
|
||||
"plots": PLOTS,
|
||||
"live": DVCLIVE,
|
||||
}
|
||||
|
||||
def run(self):
|
||||
from dvc.command.stage import parse_cmd
|
||||
|
||||
cmd = parse_cmd(self.args.cmd)
|
||||
if not cmd:
|
||||
if not self.args.interactive and not cmd:
|
||||
raise InvalidArgumentError("command is not specified")
|
||||
if self.args.interactive:
|
||||
raise NotImplementedError(
|
||||
"'-i/--interactive' is not implemented yet."
|
||||
)
|
||||
if self.args.explicit:
|
||||
raise NotImplementedError("'--explicit' is not implemented yet.")
|
||||
if self.args.template:
|
||||
raise NotImplementedError("template is not supported yet.")
|
||||
|
||||
from dvc.utils.serialize import LOADERS
|
||||
from dvc.repo.experiments.init import init
|
||||
|
||||
code = self.args.code or self.CODE
|
||||
data = self.args.data or self.DATA
|
||||
models = self.args.models or self.MODELS
|
||||
metrics = self.args.metrics or self.DEFAULT_METRICS
|
||||
params_path = self.args.params or self.DEFAULT_PARAMS
|
||||
plots = self.args.plots or self.PLOTS
|
||||
dvclive = self.args.live or self.DVCLIVE
|
||||
defaults = {}
|
||||
if not self.args.explicit:
|
||||
config = self.repo.config["exp"]
|
||||
defaults.update({**self.DEFAULTS, **config})
|
||||
|
||||
_, ext = os.path.splitext(params_path)
|
||||
params = list(LOADERS[ext](params_path))
|
||||
|
||||
name = self.args.name or self.DEFAULT_NAME
|
||||
stage = self.repo.stage.add(
|
||||
name=name,
|
||||
cmd=cmd,
|
||||
deps=[code, data],
|
||||
outs=[models],
|
||||
params=[{params_path: params}],
|
||||
metrics_no_cache=[metrics],
|
||||
plots_no_cache=[plots],
|
||||
live=dvclive,
|
||||
force=True,
|
||||
cli_args = compact(
|
||||
{
|
||||
"cmd": cmd,
|
||||
"code": self.args.code,
|
||||
"data": self.args.data,
|
||||
"models": self.args.models,
|
||||
"metrics": self.args.metrics,
|
||||
"params": self.args.params,
|
||||
"plots": self.args.plots,
|
||||
"live": self.args.live,
|
||||
}
|
||||
)
|
||||
|
||||
initialized_stage = init(
|
||||
self.repo,
|
||||
name=self.args.name,
|
||||
type=self.args.type,
|
||||
defaults=defaults,
|
||||
overrides=cli_args,
|
||||
interactive=self.args.interactive,
|
||||
force=self.args.force,
|
||||
)
|
||||
if self.args.run:
|
||||
return self.repo.experiments.run(targets=[stage.addressing])
|
||||
return self.repo.experiments.run(
|
||||
targets=[initialized_stage.addressing]
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
class RawDefaultsHelpFormatter(
|
||||
argparse.RawDescriptionHelpFormatter,
|
||||
argparse.ArgumentDefaultsHelpFormatter,
|
||||
):
|
||||
def _get_help_string(self, action: argparse.Action) -> Optional[str]:
|
||||
if action.default:
|
||||
return super()._get_help_string(action)
|
||||
return action.help
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
EXPERIMENTS_HELP = "Commands to run and compare experiments."
|
||||
|
||||
@@ -968,12 +973,14 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Always show git commit SHAs instead of branch/tag names.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output in JSON format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--csv",
|
||||
"--show-csv",
|
||||
action="store_true",
|
||||
default=False,
|
||||
@@ -1042,15 +1049,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show only params that are stage dependencies.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
@@ -1365,7 +1375,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"init",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_INIT_HELP, "exp/init"),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
formatter_class=RawDefaultsHelpFormatter,
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"cmd",
|
||||
@@ -1385,40 +1395,64 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Prompt for values that are not provided",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--template", help="Stage template to use to fill with provided values"
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Overwrite existing stage",
|
||||
)
|
||||
|
||||
experiments_init_parser.add_argument(
|
||||
"--explicit", help="Only use the path values explicitly provided"
|
||||
"--explicit",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only use the path values explicitly provided",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--name", "-n", help="Name of the stage to create"
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--code",
|
||||
default=CmdExperimentsInit.CODE,
|
||||
help="Path to the source file or directory "
|
||||
"which your experiments depend",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--data",
|
||||
default=CmdExperimentsInit.DATA,
|
||||
help="Path to the data file or directory "
|
||||
"which your experiments depend",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--models",
|
||||
default=CmdExperimentsInit.MODELS,
|
||||
help="Path to the model file or directory for your experiments",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--params", help="Path to the parameters file for your experiments"
|
||||
"--params",
|
||||
default=CmdExperimentsInit.DEFAULT_PARAMS,
|
||||
help="Path to the parameters file for your experiments",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--metrics", help="Path to the metrics file for your experiments"
|
||||
"--metrics",
|
||||
default=CmdExperimentsInit.DEFAULT_METRICS,
|
||||
help="Path to the metrics file for your experiments",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--plots",
|
||||
default=CmdExperimentsInit.PLOTS,
|
||||
help="Path to the plots file or directory for your experiments",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--live", help="Path to log dvclive outputs for your experiments"
|
||||
"--live",
|
||||
help="Path to log dvclive outputs for your experiments "
|
||||
f"(default: {CmdExperimentsInit.DVCLIVE})",
|
||||
)
|
||||
experiments_init_parser.add_argument(
|
||||
"--type",
|
||||
choices=["default", "live"],
|
||||
default="default",
|
||||
help="Select type of stage to create",
|
||||
)
|
||||
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
|
||||
|
||||
|
||||
@@ -34,10 +34,8 @@ class CmdList(CmdBaseNoRepo):
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
)
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(entries))
|
||||
if self.args.json:
|
||||
ui.write_json(entries)
|
||||
elif entries:
|
||||
entries = _prettify(entries, with_color=True)
|
||||
ui.write("\n".join(entries))
|
||||
@@ -54,6 +52,7 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
list_parser = subparsers.add_parser(
|
||||
"list",
|
||||
aliases=["ls"],
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(LIST_HELP, "list"),
|
||||
help=LIST_HELP,
|
||||
@@ -70,7 +69,10 @@ def add_parser(subparsers, parent_parser):
|
||||
"--dvc-only", action="store_true", help="Show only DVC outputs."
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--show-json", action="store_true", help="Show output in JSON format."
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--rev",
|
||||
|
||||
+12
-10
@@ -31,16 +31,14 @@ class CmdMetricsShow(CmdMetricsBase):
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(metrics, default=encode_exception))
|
||||
if self.args.json:
|
||||
ui.write_json(metrics, default=encode_exception)
|
||||
else:
|
||||
from dvc.compare import show_metrics
|
||||
|
||||
show_metrics(
|
||||
metrics,
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
@@ -65,17 +63,15 @@ class CmdMetricsDiff(CmdMetricsBase):
|
||||
logger.exception("failed to show metrics diff")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(diff))
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
show_diff(
|
||||
diff,
|
||||
title="Metric",
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
precision=self.args.precision or DEFAULT_PRECISION,
|
||||
round_digits=True,
|
||||
@@ -140,15 +136,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show metrics for all commits.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
@@ -220,15 +219,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show unchanged metrics as well.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
|
||||
@@ -25,17 +25,15 @@ class CmdParamsDiff(CmdBase):
|
||||
logger.exception("failed to show params diff")
|
||||
return 1
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(diff))
|
||||
if self.args.json:
|
||||
ui.write_json(diff)
|
||||
else:
|
||||
from dvc.compare import show_diff
|
||||
|
||||
show_diff(
|
||||
diff,
|
||||
title="Param",
|
||||
markdown=self.args.show_md,
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
show_changes=False,
|
||||
)
|
||||
@@ -103,15 +101,18 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show only params that are stage dependencies.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--json",
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
|
||||
@@ -86,7 +86,7 @@ class CmdRemoteDefault(CmdRemote):
|
||||
if self.args.name is None and not self.args.unset:
|
||||
conf = self.config.read(self.args.level)
|
||||
try:
|
||||
print(conf["core"]["remote"])
|
||||
ui.write(conf["core"]["remote"])
|
||||
except KeyError:
|
||||
ui.write("No default remote set")
|
||||
return 1
|
||||
|
||||
@@ -69,10 +69,8 @@ class CmdDataStatus(CmdDataBase):
|
||||
if self.args.quiet:
|
||||
return bool(st)
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
ui.write(json.dumps(st))
|
||||
if self.args.json:
|
||||
ui.write_json(st)
|
||||
return 0
|
||||
|
||||
if st:
|
||||
|
||||
+1
-1
@@ -168,7 +168,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
def render(self, **kwargs: Any):
|
||||
from dvc.ui import ui
|
||||
|
||||
if kwargs.pop("show_csv", False):
|
||||
if kwargs.pop("csv", False):
|
||||
ui.write(self.to_csv(), end="")
|
||||
else:
|
||||
ui.table(self, headers=self.keys(), **kwargs)
|
||||
|
||||
@@ -156,6 +156,8 @@ SCHEMA = {
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
"cache_regions": bool,
|
||||
"read_timeout": Coerce(int),
|
||||
"connect_timeout": Coerce(int),
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
@@ -223,9 +225,13 @@ SCHEMA = {
|
||||
)
|
||||
},
|
||||
"state": {
|
||||
"dir": str,
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str,
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
"cloud": All(Lower, Choices("aws", "azure")),
|
||||
@@ -250,4 +256,13 @@ SCHEMA = {
|
||||
Optional("parametrization", default=True): Bool,
|
||||
},
|
||||
"plots": {"html_template": str},
|
||||
"exp": {
|
||||
"code": str,
|
||||
"data": str,
|
||||
"models": str,
|
||||
"metrics": str,
|
||||
"params": str,
|
||||
"plots": str,
|
||||
"live": str,
|
||||
},
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ class DataCloud:
|
||||
from dvc.objects.db import get_odb
|
||||
|
||||
cls, config, path_info = get_cloud_fs(self.repo, name=name)
|
||||
config["tmp_dir"] = self.repo.tmp_dir
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return get_odb(cls(**config), path_info, **config)
|
||||
|
||||
def push(
|
||||
|
||||
@@ -63,7 +63,7 @@ class RepoDependency(Dependency):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
from dvc.checkout import checkout
|
||||
from dvc.objects.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
|
||||
+34
-52
@@ -1,10 +1,7 @@
|
||||
import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, Union
|
||||
|
||||
from voluptuous import MultipleInvalid
|
||||
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
|
||||
@@ -12,23 +9,18 @@ from dvc.stage import serialize
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileBadNameError,
|
||||
StageFileDoesNotExistError,
|
||||
StageFileFormatError,
|
||||
StageFileIsNotDvcFileError,
|
||||
)
|
||||
from dvc.types import AnyPath
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.collections import apply_diff
|
||||
from dvc.utils.serialize import (
|
||||
dump_yaml,
|
||||
modify_yaml,
|
||||
parse_yaml,
|
||||
parse_yaml_for_update,
|
||||
)
|
||||
from dvc.utils.serialize import dump_yaml, modify_yaml
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_T = TypeVar("_T")
|
||||
|
||||
DVC_FILE = "Dvcfile"
|
||||
DVC_FILE_SUFFIX = ".dvc"
|
||||
@@ -45,10 +37,6 @@ class FileIsGitIgnored(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class LockfileCorruptedError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class ParametrizedDumpError(DvcException):
|
||||
pass
|
||||
|
||||
@@ -96,7 +84,7 @@ def check_dvcfile_path(repo, path):
|
||||
|
||||
|
||||
class FileMixin:
|
||||
SCHEMA = None
|
||||
SCHEMA: Callable[[_T], _T]
|
||||
|
||||
def __init__(self, repo, path, verify=True, **kwargs):
|
||||
self.repo = repo
|
||||
@@ -138,7 +126,11 @@ class FileMixin:
|
||||
if self._is_git_ignored():
|
||||
raise FileIsGitIgnored(self.path)
|
||||
|
||||
def _load(self):
|
||||
def load(self, **kwargs: Any) -> Any:
|
||||
d, _ = self._load(**kwargs)
|
||||
return d
|
||||
|
||||
def _load(self, **kwargs: Any) -> Tuple[Any, str]:
|
||||
# it raises the proper exceptions by priority:
|
||||
# 1. when the file doesn't exists
|
||||
# 2. filename is not a DVC file
|
||||
@@ -155,18 +147,23 @@ class FileMixin:
|
||||
raise StageFileIsNotDvcFileError(self.path)
|
||||
|
||||
self._check_gitignored()
|
||||
with self.repo.fs.open(self.path, encoding="utf-8") as fd:
|
||||
stage_text = fd.read()
|
||||
d = parse_yaml(stage_text, self.path)
|
||||
return self.validate(d, self.relpath), stage_text
|
||||
return self._load_yaml(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, d, fname=None):
|
||||
assert isinstance(cls.SCHEMA, collections.abc.Callable)
|
||||
try:
|
||||
return cls.SCHEMA(d) # pylint: disable=not-callable
|
||||
except MultipleInvalid as exc:
|
||||
raise StageFileFormatError(f"'{fname}' format error: {exc}")
|
||||
def validate(cls, d: _T, fname: str = None) -> _T:
|
||||
from dvc.utils.strictyaml import validate
|
||||
|
||||
return validate(d, cls.SCHEMA, path=fname) # type: ignore[arg-type]
|
||||
|
||||
def _load_yaml(self, **kwargs: Any) -> Tuple[Any, str]:
|
||||
from dvc.utils import strictyaml
|
||||
|
||||
return strictyaml.load(
|
||||
self.path,
|
||||
self.SCHEMA, # type: ignore[arg-type]
|
||||
self.repo.fs,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def remove(self, force=False): # pylint: disable=unused-argument
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
@@ -298,10 +295,7 @@ class PipelineFile(FileMixin):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
d = parse_yaml_for_update(f.read(), self.path)
|
||||
|
||||
self.validate(d, self.path)
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
if stage.name not in d.get("stages", {}):
|
||||
return
|
||||
|
||||
@@ -343,34 +337,25 @@ def migrate_lock_v1_to_v2(d, version_info):
|
||||
d["stages"] = stages
|
||||
|
||||
|
||||
def lockfile_schema(data: _T) -> _T:
|
||||
schema = get_lockfile_schema(data)
|
||||
return schema(data)
|
||||
|
||||
|
||||
class Lockfile(FileMixin):
|
||||
@classmethod
|
||||
def validate(cls, d, fname=None):
|
||||
schema = get_lockfile_schema(d)
|
||||
try:
|
||||
return schema(d)
|
||||
except MultipleInvalid as exc:
|
||||
raise StageFileFormatError(f"'{fname}' format error: {exc}")
|
||||
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
|
||||
|
||||
def _verify_filename(self):
|
||||
pass # lockfile path is hardcoded, so no need to verify here
|
||||
|
||||
def _load(self):
|
||||
def _load(self, **kwargs: Any):
|
||||
try:
|
||||
return super()._load()
|
||||
return super()._load(**kwargs)
|
||||
except StageFileDoesNotExistError:
|
||||
# we still need to account for git-ignored dvc.lock file
|
||||
# even though it may not exist or have been .dvcignored
|
||||
self._check_gitignored()
|
||||
return {}, ""
|
||||
except StageFileFormatError as exc:
|
||||
raise LockfileCorruptedError(
|
||||
f"Lockfile '{self.relpath}' is corrupted."
|
||||
) from exc
|
||||
|
||||
def load(self):
|
||||
d, _ = self._load()
|
||||
return d
|
||||
|
||||
@property
|
||||
def latest_version_info(self):
|
||||
@@ -409,10 +394,7 @@ class Lockfile(FileMixin):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
with open(self.path, encoding="utf-8") as f:
|
||||
d = parse_yaml_for_update(f.read(), self.path)
|
||||
self.validate(d, self.path)
|
||||
|
||||
d, _ = self._load_yaml(round_trip=True)
|
||||
version = LOCKFILE_VERSION.from_dict(d)
|
||||
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
|
||||
if stage.name not in data:
|
||||
|
||||
@@ -364,3 +364,8 @@ class CircularImportError(DvcException):
|
||||
f"'{dep}' contains invalid circular import. "
|
||||
f"DVC repo '{a}' already imports from '{b}'."
|
||||
)
|
||||
|
||||
|
||||
class PrettyDvcException(DvcException):
|
||||
def __pretty_exc__(self, **kwargs):
|
||||
"""Print prettier exception message."""
|
||||
|
||||
+2
-15
@@ -6,7 +6,6 @@ from functools import partialmethod
|
||||
from multiprocessing import cpu_count
|
||||
from typing import Any, ClassVar, Dict, FrozenSet, Optional
|
||||
|
||||
from funcy import cached_property
|
||||
from tqdm.utils import CallbackIOWrapper
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -176,12 +175,6 @@ class BaseFileSystem:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def ls(self, path_info, detail=False):
|
||||
raise RemoteActionNotImplemented("ls", self.scheme)
|
||||
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
raise RemoteActionNotImplemented("find", self.scheme)
|
||||
|
||||
def is_empty(self, path_info):
|
||||
return False
|
||||
|
||||
@@ -223,12 +216,6 @@ class BaseFileSystem:
|
||||
return False
|
||||
return hash_.endswith(cls.CHECKSUM_DIR_SUFFIX)
|
||||
|
||||
@cached_property
|
||||
def _local_fs(self):
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
return LocalFileSystem()
|
||||
|
||||
def upload(
|
||||
self,
|
||||
from_info,
|
||||
@@ -260,7 +247,7 @@ class BaseFileSystem:
|
||||
**pbar_args,
|
||||
)
|
||||
stack.enter_context(pbar)
|
||||
callback = pbar.as_callback(self._local_fs, from_info)
|
||||
callback = pbar.as_callback()
|
||||
if total:
|
||||
callback.set_size(total)
|
||||
|
||||
@@ -317,7 +304,7 @@ class BaseFileSystem:
|
||||
**pbar_kwargs,
|
||||
)
|
||||
stack.enter_context(pbar)
|
||||
callback = pbar.as_callback(self, from_info)
|
||||
callback = pbar.as_callback()
|
||||
|
||||
with stack:
|
||||
if download_dir:
|
||||
|
||||
+121
-45
@@ -1,14 +1,37 @@
|
||||
import os
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterator,
|
||||
Optional,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from funcy import cached_property
|
||||
from tqdm.utils import CallbackIOWrapper
|
||||
|
||||
from dvc.path_info import URLInfo
|
||||
from dvc.progress import DEFAULT_CALLBACK
|
||||
|
||||
from .base import BaseFileSystem
|
||||
from .local import LocalFileSystem
|
||||
|
||||
FSPath = URLInfo
|
||||
AnyFSPath = Union[FSPath, str]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
# An info() entry, might evolve to a TypedDict
|
||||
# in the future (e.g for properly type 'size' etc).
|
||||
Entry = Dict[str, Any]
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
@@ -20,18 +43,8 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
self.fs_args = {"skip_instance_cache": True}
|
||||
self.fs_args.update(self._prepare_credentials(**kwargs))
|
||||
|
||||
@cached_property
|
||||
def fs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _with_bucket(self, path):
|
||||
return str(path)
|
||||
|
||||
def _strip_bucket(self, entry):
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
def _get_kwargs_from_urls(urlpath: str) -> "Dict[str, Any]":
|
||||
from fsspec.utils import infer_storage_options
|
||||
|
||||
options = infer_storage_options(urlpath)
|
||||
@@ -39,6 +52,28 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
options.pop("protocol", None)
|
||||
return options
|
||||
|
||||
@cached_property
|
||||
def fs(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _with_bucket(self, path: AnyFSPath) -> str:
|
||||
return str(path)
|
||||
|
||||
def _strip_bucket(self, entry: str) -> str:
|
||||
return entry
|
||||
|
||||
@overload
|
||||
def _strip_buckets(
|
||||
self, entries: "Iterable[Entry]", detail: "Literal[True]"
|
||||
) -> "Iterator[Entry]":
|
||||
...
|
||||
|
||||
@overload
|
||||
def _strip_buckets(
|
||||
self, entries: "Iterable[str]", detail: "Literal[False]" = False
|
||||
) -> "Iterator[str]":
|
||||
...
|
||||
|
||||
def _strip_buckets(self, entries, detail=False):
|
||||
for entry in entries:
|
||||
if detail:
|
||||
@@ -48,59 +83,91 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
entry = self._strip_bucket(entry)
|
||||
yield entry
|
||||
|
||||
def _entry_hook(self, entry):
|
||||
def _entry_hook(self, entry: "Entry") -> "Entry":
|
||||
"""Simple hook method to be overridden when wanted to process
|
||||
entries within info() and ls(detail=True) calls"""
|
||||
return entry
|
||||
|
||||
def _prepare_credentials(
|
||||
self, **config
|
||||
): # pylint: disable=unused-argument
|
||||
self, **config: Dict[str, Any] # pylint: disable=unused-argument
|
||||
) -> Dict[str, Any]:
|
||||
"""Prepare the arguments for authentication to the
|
||||
host filesystem"""
|
||||
return {}
|
||||
|
||||
def _isdir(self, path_info):
|
||||
def _isdir(self, path_info: FSPath) -> bool:
|
||||
return self.fs.isdir(self._with_bucket(path_info))
|
||||
|
||||
def isdir(self, path_info):
|
||||
def isdir(self, path_info: FSPath) -> bool:
|
||||
try:
|
||||
return self._isdir(path_info)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def isfile(self, path_info):
|
||||
def isfile(self, path_info: FSPath) -> bool:
|
||||
try:
|
||||
return not self._isdir(path_info)
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def is_empty(self, path_info):
|
||||
def is_empty(self, path_info: FSPath) -> bool:
|
||||
entry = self.info(path_info)
|
||||
if entry["type"] == "directory":
|
||||
return not self.fs.ls(self._with_bucket(path_info))
|
||||
return entry["size"] == 0
|
||||
|
||||
def open(
|
||||
self, path_info, mode="r", **kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
self,
|
||||
path_info: FSPath,
|
||||
mode: str = "r",
|
||||
encoding: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "IO": # pylint: disable=arguments-differ
|
||||
return self.fs.open(self._with_bucket(path_info), mode=mode)
|
||||
|
||||
def checksum(self, path_info):
|
||||
def checksum(self, path_info: FSPath) -> str:
|
||||
return self.fs.checksum(self._with_bucket(path_info))
|
||||
|
||||
def copy(self, from_info, to_info):
|
||||
def copy(self, from_info: FSPath, to_info: FSPath) -> None:
|
||||
self.makedirs(to_info.parent)
|
||||
self.fs.copy(self._with_bucket(from_info), self._with_bucket(to_info))
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
def exists(self, path_info: FSPath) -> bool:
|
||||
return self.fs.exists(self._with_bucket(path_info))
|
||||
|
||||
@overload
|
||||
def ls(
|
||||
self, path_info: FSPath, detail: "Literal[True]"
|
||||
) -> "Iterator[Entry]":
|
||||
...
|
||||
|
||||
@overload
|
||||
def ls(self, path_info: FSPath, detail: "Literal[False]") -> Iterator[str]:
|
||||
...
|
||||
|
||||
def ls(self, path_info, detail=False):
|
||||
path = self._with_bucket(path_info)
|
||||
files = self.fs.ls(path, detail=detail)
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
|
||||
@overload
|
||||
def find(
|
||||
self,
|
||||
path_info: FSPath,
|
||||
detail: "Literal[True]",
|
||||
prefix: Optional[bool],
|
||||
) -> "Iterator[Entry]":
|
||||
...
|
||||
|
||||
@overload
|
||||
def find(
|
||||
self,
|
||||
path_info: FSPath,
|
||||
detail: "Literal[False]",
|
||||
prefix: Optional[bool],
|
||||
) -> Iterator[str]:
|
||||
...
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def find(self, path_info, detail=False, prefix=None):
|
||||
path = self._with_bucket(path_info)
|
||||
@@ -110,46 +177,56 @@ class FSSpecWrapper(BaseFileSystem):
|
||||
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
def walk_files(self, path_info: FSPath, **kwargs) -> Iterator[FSPath]:
|
||||
for file in self.find(path_info, **kwargs):
|
||||
yield path_info.replace(path=file)
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
def move(self, from_info: FSPath, to_info: FSPath) -> None:
|
||||
self.fs.move(self._with_bucket(from_info), self._with_bucket(to_info))
|
||||
|
||||
def remove(self, path_info):
|
||||
def remove(self, path_info: FSPath) -> None:
|
||||
self.fs.rm_file(self._with_bucket(path_info))
|
||||
|
||||
def info(self, path_info):
|
||||
def info(self, path_info: FSPath) -> "Entry":
|
||||
info = self.fs.info(self._with_bucket(path_info)).copy()
|
||||
info = self._entry_hook(info)
|
||||
info["name"] = self._strip_bucket(info["name"])
|
||||
return info
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
def makedirs(self, path_info: FSPath, **kwargs) -> None:
|
||||
self.fs.makedirs(
|
||||
self._with_bucket(path_info), exist_ok=kwargs.pop("exist_ok", True)
|
||||
)
|
||||
|
||||
def put_file(
|
||||
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
self,
|
||||
from_file: FSPath,
|
||||
to_info: FSPath,
|
||||
callback: Any = DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.fs.put_file(
|
||||
from_file, self._with_bucket(to_info), callback=callback, **kwargs
|
||||
)
|
||||
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
|
||||
|
||||
def get_file(
|
||||
self, from_info, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
self,
|
||||
from_info: FSPath,
|
||||
to_info: FSPath,
|
||||
callback: Any = DEFAULT_CALLBACK,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.fs.get_file(
|
||||
self._with_bucket(from_info), to_info, callback=callback, **kwargs
|
||||
)
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
def upload_fobj(self, fobj: IO, to_info: FSPath, **kwargs) -> None:
|
||||
self.makedirs(to_info.parent)
|
||||
with self.open(to_info, "wb") as fdest:
|
||||
shutil.copyfileobj(fobj, fdest, length=fdest.blocksize)
|
||||
shutil.copyfileobj(
|
||||
fobj, fdest, length=getattr(fdest, "blocksize", None)
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
@@ -157,21 +234,21 @@ class ObjectFSWrapper(FSSpecWrapper):
|
||||
TRAVERSE_PREFIX_LEN = 3
|
||||
|
||||
@lru_cache(512)
|
||||
def _with_bucket(self, path):
|
||||
def _with_bucket(self, path: AnyFSPath) -> str:
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return f"{path.bucket}/{path.path}"
|
||||
return path
|
||||
return str(path)
|
||||
|
||||
def _strip_bucket(self, entry):
|
||||
def _strip_bucket(self, entry: str) -> str:
|
||||
try:
|
||||
bucket, path = entry.split("/", 1)
|
||||
except ValueError:
|
||||
# If there is no path attached, only returns
|
||||
# the bucket (top-level).
|
||||
bucket, path = entry, None
|
||||
bucket, path = entry, ""
|
||||
return path or bucket
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
def makedirs(self, path_info: FSPath, **kwargs) -> None:
|
||||
# For object storages make this method a no-op. The original
|
||||
# fs.makedirs() method will only check if the bucket exists
|
||||
# and create if it doesn't though we don't want to support
|
||||
@@ -179,7 +256,7 @@ class ObjectFSWrapper(FSSpecWrapper):
|
||||
# simply ignore all mkdir()/makedirs() calls.
|
||||
return None
|
||||
|
||||
def _isdir(self, path_info):
|
||||
def _isdir(self, path_info: FSPath) -> bool:
|
||||
# Directory in object storages are interpreted differently
|
||||
# among different fsspec providers, so this logic is a temporary
|
||||
# measure for us to adapt as of now. It checks whether it is a
|
||||
@@ -214,7 +291,9 @@ class ObjectFSWrapper(FSSpecWrapper):
|
||||
if len(files) == 1 and files[0] == path and self.isdir(path_info):
|
||||
return None
|
||||
|
||||
yield from self._strip_buckets(files, detail=detail)
|
||||
yield from self._strip_buckets( # pylint: disable=not-an-iterable
|
||||
files, detail=detail
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=arguments-differ
|
||||
@@ -238,9 +317,6 @@ class NoDirectoriesMixin:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
_LOCAL_FS = LocalFileSystem()
|
||||
|
||||
|
||||
class CallbackMixin:
|
||||
"""Provides callback support for the filesystem that don't support yet."""
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ class GSFileSystem(CallbackMixin, ObjectFSWrapper):
|
||||
REQUIRES = {"gcsfs": "gcsfs"}
|
||||
PARAM_CHECKSUM = "etag"
|
||||
DETAIL_FIELDS = frozenset(("etag", "size"))
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
login_info = {"consistency": None}
|
||||
|
||||
+60
-252
@@ -1,282 +1,90 @@
|
||||
import errno
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import deque
|
||||
from contextlib import closing, contextmanager
|
||||
import threading
|
||||
|
||||
from tqdm.utils import CallbackIOWrapper
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.progress import DEFAULT_CALLBACK
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.utils import fix_env, tmp_fname
|
||||
from dvc.utils import fix_env
|
||||
|
||||
from .base import BaseFileSystem, RemoteCmdError
|
||||
from .pool import get_connection
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
CHECKSUM_REGEX = re.compile(r".*\t.*\t(?P<checksum>.*)")
|
||||
|
||||
|
||||
def _hadoop_fs(cmd, user=None):
|
||||
cmd = "hadoop fs -" + cmd
|
||||
if user:
|
||||
cmd = f"HADOOP_USER_NAME={user} " + cmd
|
||||
|
||||
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
|
||||
# See https://github.com/iterative/dvc/issues/1197.
|
||||
close_fds = os.name != "nt"
|
||||
|
||||
executable = os.getenv("SHELL") if os.name != "nt" else None
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
shell=True,
|
||||
close_fds=close_fds,
|
||||
executable=executable,
|
||||
env=fix_env(os.environ),
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = p.communicate()
|
||||
if p.returncode != 0:
|
||||
raise RemoteCmdError("hdfs", cmd, p.returncode, err)
|
||||
return out.decode("utf-8")
|
||||
|
||||
|
||||
def _group(regex, s, gname):
|
||||
match = re.match(regex, s)
|
||||
assert match is not None
|
||||
return match.group(gname)
|
||||
|
||||
|
||||
def _hadoop_fs_checksum(path_info):
|
||||
# NOTE: pyarrow doesn't support checksum, so we need to use hadoop
|
||||
regex = r".*\t.*\t(?P<checksum>.*)"
|
||||
stdout = _hadoop_fs(f"checksum {path_info.url}", user=path_info.user)
|
||||
return _group(regex, stdout, "checksum")
|
||||
|
||||
|
||||
class HDFSFileSystem(BaseFileSystem):
|
||||
# pylint: disable=abstract-method
|
||||
class HDFSFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
scheme = Schemes.HDFS
|
||||
REQUIRES = {"pyarrow": "pyarrow"}
|
||||
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
|
||||
REQUIRES = {"fsspec": "fsspec", "pyarrow": "pyarrow"}
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
BLOCK_SIZE = 2 ** 26 # 128MB
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self.host = config["host"]
|
||||
self.user = config.get("user")
|
||||
self.port = config.get("port")
|
||||
|
||||
self.kerb_ticket = config.get("kerb_ticket")
|
||||
def _with_bucket(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return path.path
|
||||
return super()._with_bucket(path)
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
from fsspec.implementations.hdfs import PyArrowHDFS
|
||||
from fsspec.implementations.arrow import HadoopFileSystem
|
||||
|
||||
# pylint:disable=protected-access
|
||||
return PyArrowHDFS._get_kwargs_from_urls(urlpath)
|
||||
return HadoopFileSystem._get_kwargs_from_urls(urlpath)
|
||||
|
||||
def hdfs(self, path_info):
|
||||
import pyarrow.fs
|
||||
def _prepare_credentials(self, **config):
|
||||
return config
|
||||
|
||||
# NOTE: HadoopFileSystem is not meant to be closed by us and doesn't
|
||||
# have close() method or alternative, so we add a noop one to satisfy
|
||||
# our connection pool.
|
||||
class HDFSConnection(pyarrow.fs.HadoopFileSystem):
|
||||
def close(self):
|
||||
pass
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def fs(self):
|
||||
from fsspec.implementations.arrow import HadoopFileSystem
|
||||
|
||||
return get_connection(
|
||||
HDFSConnection,
|
||||
path_info.host,
|
||||
path_info.port,
|
||||
user=path_info.user,
|
||||
kerb_ticket=self.kerb_ticket,
|
||||
)
|
||||
|
||||
@contextmanager
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
|
||||
try:
|
||||
with self.hdfs(path_info) as hdfs, closing(
|
||||
hdfs.open_input_stream(path_info.path)
|
||||
) as fd:
|
||||
if mode == "rb":
|
||||
yield fd
|
||||
else:
|
||||
yield io.TextIOWrapper(fd, encoding=encoding)
|
||||
except OSError as e:
|
||||
# Empty .errno and not specific enough error class in pyarrow,
|
||||
# see https://issues.apache.org/jira/browse/ARROW-6248
|
||||
if "file does not exist" in str(e):
|
||||
raise FileNotFoundError(*e.args)
|
||||
raise
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
assert not isinstance(path_info, list)
|
||||
assert path_info.scheme == "hdfs"
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
return file_info.type != pyarrow.fs.FileType.NotFound
|
||||
|
||||
def _walk(self, hdfs, root, topdown=True):
|
||||
import posixpath
|
||||
|
||||
from pyarrow.fs import FileSelector, FileType
|
||||
|
||||
dirs = deque()
|
||||
nondirs = deque()
|
||||
|
||||
selector = FileSelector(root)
|
||||
for entry in hdfs.get_file_info(selector):
|
||||
if entry.type == FileType.Directory:
|
||||
dirs.append(entry.base_name)
|
||||
else:
|
||||
nondirs.append(entry.base_name)
|
||||
|
||||
if topdown:
|
||||
yield root, dirs, nondirs
|
||||
|
||||
for dname in dirs:
|
||||
# NOTE: posixpath.join() is slower
|
||||
yield from self._walk(
|
||||
hdfs, f"{root}{posixpath.sep}{dname}", topdown=topdown
|
||||
)
|
||||
|
||||
if not topdown:
|
||||
yield root, dirs, nondirs
|
||||
|
||||
def walk(self, top, topdown=True, onerror=None, **kwargs):
|
||||
if not self.isdir(top):
|
||||
return
|
||||
|
||||
with self.hdfs(top) as hdfs:
|
||||
for root, dnames, fnames in self._walk(
|
||||
hdfs, top.path, topdown=topdown
|
||||
):
|
||||
yield top.replace(path=root), dnames, fnames
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
for root, _, fnames in self.walk(path_info):
|
||||
yield from (root / fname for fname in fnames)
|
||||
|
||||
def remove(self, path_info):
|
||||
if path_info.scheme != "hdfs":
|
||||
raise NotImplementedError
|
||||
|
||||
if self.exists(path_info):
|
||||
logger.debug(f"Removing {path_info.path}")
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
if file_info.type == pyarrow.fs.FileType.Directory:
|
||||
hdfs.delete_dir(path_info.path)
|
||||
else:
|
||||
hdfs.delete_file(path_info.path)
|
||||
|
||||
def makedirs(self, path_info, **kwargs):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
# NOTE: fs.create_dir creates parents by default
|
||||
hdfs.create_dir(path_info.path)
|
||||
|
||||
def copy(self, from_info, to_info, **_kwargs):
|
||||
# NOTE: hdfs.copy_file is not supported yet in pyarrow
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
# NOTE: this is how `hadoop fs -cp` works too: it copies through
|
||||
# your local machine.
|
||||
with closing(hdfs.open_input_stream(from_info.path)) as from_fobj:
|
||||
tmp_info = to_info.parent / tmp_fname(to_info.name)
|
||||
try:
|
||||
with closing(
|
||||
hdfs.open_output_stream(tmp_info.path)
|
||||
) as tmp_fobj:
|
||||
shutil.copyfileobj(from_fobj, tmp_fobj)
|
||||
hdfs.move(tmp_info.path, to_info.path)
|
||||
except Exception:
|
||||
self.remove(tmp_info)
|
||||
raise
|
||||
|
||||
def isfile(self, path_info):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
return file_info.type == pyarrow.fs.FileType.File
|
||||
|
||||
def isdir(self, path_info):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
import pyarrow.fs
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
return file_info.type == pyarrow.fs.FileType.Directory
|
||||
|
||||
def info(self, path_info):
|
||||
with self.hdfs(path_info) as hdfs:
|
||||
from pyarrow.fs import FileType
|
||||
|
||||
file_info = hdfs.get_file_info(path_info.path)
|
||||
if file_info.type is FileType.Directory:
|
||||
kind = "directory"
|
||||
elif file_info.type is FileType.File:
|
||||
kind = "file"
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
errno.ENOENT, os.strerror(errno.ENOENT), path_info
|
||||
)
|
||||
|
||||
return {"size": file_info.size, "type": kind}
|
||||
return HadoopFileSystem(**self.fs_args)
|
||||
|
||||
def checksum(self, path_info):
|
||||
return HashInfo(
|
||||
"checksum",
|
||||
_hadoop_fs_checksum(path_info),
|
||||
size=self.getsize(path_info),
|
||||
return self._checksum(path_info)
|
||||
|
||||
def _checksum(self, path_info, **kwargs):
|
||||
# PyArrow doesn't natively support retrieving the
|
||||
# checksum, so we have to use hadoop fs
|
||||
|
||||
result = self._run_command(
|
||||
f"checksum {path_info.url}",
|
||||
env=fix_env(os.environ),
|
||||
user=path_info.user,
|
||||
)
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
with hdfs.open_output_stream(to_info.path) as fdest:
|
||||
shutil.copyfileobj(fobj, fdest, self.BLOCK_SIZE)
|
||||
match = CHECKSUM_REGEX.match(result)
|
||||
if match is None:
|
||||
return None
|
||||
|
||||
def put_file(
|
||||
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
with self.hdfs(to_info) as hdfs:
|
||||
hdfs.create_dir(to_info.parent.path)
|
||||
return match.group("checksum")
|
||||
|
||||
tmp_file = tmp_fname(to_info.path)
|
||||
total: int = os.path.getsize(from_file)
|
||||
callback.set_size(total)
|
||||
def _run_command(self, cmd, env=None, user=None):
|
||||
cmd = "hadoop fs -" + cmd
|
||||
if user:
|
||||
cmd = f"HADOOP_USER_NAME={user} " + cmd
|
||||
|
||||
with open(from_file, "rb") as fobj:
|
||||
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
|
||||
with hdfs.open_output_stream(tmp_file) as sobj:
|
||||
shutil.copyfileobj(wrapped, sobj, self.BLOCK_SIZE)
|
||||
hdfs.move(tmp_file, to_info.path)
|
||||
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
|
||||
# See https://github.com/iterative/dvc/issues/1197.
|
||||
close_fds = os.name != "nt"
|
||||
|
||||
def get_file(
|
||||
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
with self.hdfs(from_info) as hdfs:
|
||||
file_info = hdfs.get_file_info(from_info.path)
|
||||
total = file_info.size
|
||||
if total:
|
||||
callback.set_size(total)
|
||||
executable = os.getenv("SHELL") if os.name != "nt" else None
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
shell=True,
|
||||
close_fds=close_fds,
|
||||
executable=executable,
|
||||
env=env or os.environ,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = p.communicate()
|
||||
|
||||
with hdfs.open_input_stream(from_info.path) as sobj:
|
||||
with open(to_file, "wb+") as fobj:
|
||||
wrapped = CallbackIOWrapper(callback.relative_update, sobj)
|
||||
shutil.copyfileobj(wrapped, fobj, self.BLOCK_SIZE)
|
||||
if p.returncode != 0:
|
||||
raise subprocess.CalledProcessError(p.returncode, cmd, out, err)
|
||||
else:
|
||||
return out.decode("utf-8")
|
||||
|
||||
@@ -87,6 +87,9 @@ class HTTPFileSystem(NoDirectoriesMixin, FSSpecWrapper):
|
||||
ssl=make_context(config["ssl_verify"])
|
||||
)
|
||||
|
||||
# Allow reading proxy configurations from the environment.
|
||||
client_kwargs["trust_env"] = True
|
||||
|
||||
credentials["get_client"] = self.get_client
|
||||
self.upload_method = config.get("method", "POST")
|
||||
return credentials
|
||||
@@ -99,6 +102,7 @@ class HTTPFileSystem(NoDirectoriesMixin, FSSpecWrapper):
|
||||
attempts=self.SESSION_RETRIES,
|
||||
factor=self.SESSION_BACKOFF_FACTOR,
|
||||
max_timeout=self.REQUEST_TIMEOUT,
|
||||
exceptions=[aiohttp.ClientError],
|
||||
)
|
||||
|
||||
# The default timeout for the aiohttp is 300 seconds
|
||||
|
||||
@@ -99,6 +99,11 @@ class BaseS3FileSystem(ObjectFSWrapper):
|
||||
client["endpoint_url"] = config.get("endpointurl")
|
||||
client["verify"] = config.get("ssl_verify")
|
||||
|
||||
# timeout configuration
|
||||
config_kwargs = login_info["config_kwargs"]
|
||||
config_kwargs["read_timeout"] = config.get("read_timeout")
|
||||
config_kwargs["connect_timeout"] = config.get("connect_timeout")
|
||||
|
||||
# encryptions
|
||||
additional = login_info["s3_additional_kwargs"]
|
||||
additional["ServerSideEncryption"] = config.get("sse")
|
||||
|
||||
+2
-4
@@ -36,10 +36,8 @@ class SSHFileSystem(FSSpecWrapper):
|
||||
|
||||
def _with_bucket(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
path_str = path.path
|
||||
else:
|
||||
path_str = super()._with_bucket(path)
|
||||
return os.path.normpath(path_str)
|
||||
return path.path
|
||||
return super()._with_bucket(path)
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
self.CAN_TRAVERSE = True
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
return path.path[len(self.prefix) :].lstrip("/")
|
||||
return path
|
||||
|
||||
_with_bucket = translate_path_info
|
||||
_with_bucket = translate_path_info # type: ignore
|
||||
|
||||
|
||||
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
|
||||
|
||||
+20
-151
@@ -1,60 +1,24 @@
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
import shutil
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.path_info import CloudURLInfo
|
||||
from dvc.progress import DEFAULT_CALLBACK
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from .base import BaseFileSystem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# pylint:disable=abstract-method
|
||||
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
|
||||
|
||||
|
||||
def update_pbar(pbar, total):
|
||||
"""Update pbar to accept the two arguments passed by hdfs"""
|
||||
|
||||
def update(_, bytes_transfered):
|
||||
if bytes_transfered == -1:
|
||||
pbar.update_to(total)
|
||||
return
|
||||
pbar.update_to(bytes_transfered)
|
||||
|
||||
return update
|
||||
|
||||
|
||||
def update_callback(callback, total):
|
||||
def update(_, bytes_transfered):
|
||||
if bytes_transfered == -1:
|
||||
return callback.absolute_update(total)
|
||||
return callback.relative_update(bytes_transfered)
|
||||
|
||||
return update
|
||||
|
||||
|
||||
class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
class WebHDFSFileSystem(CallbackMixin, FSSpecWrapper):
|
||||
scheme = Schemes.WEBHDFS
|
||||
PATH_CLS = CloudURLInfo
|
||||
REQUIRES = {"hdfs": "hdfs"}
|
||||
REQUIRES = {"fsspec": "fsspec"}
|
||||
PARAM_CHECKSUM = "checksum"
|
||||
TRAVERSE_PREFIX_LEN = 2
|
||||
|
||||
def __init__(self, **config):
|
||||
super().__init__(**config)
|
||||
|
||||
self.host = config["host"]
|
||||
self.user = config.get("user")
|
||||
self.port = config.get("port")
|
||||
|
||||
self.hdfscli_config = config.get("hdfscli_config")
|
||||
self.token = config.get("webhdfs_token")
|
||||
self.alias = config.get("webhdfs_alias")
|
||||
def _with_bucket(self, path):
|
||||
if isinstance(path, self.PATH_CLS):
|
||||
return f"/{path.path.rstrip('/')}"
|
||||
return path
|
||||
|
||||
@staticmethod
|
||||
def _get_kwargs_from_urls(urlpath):
|
||||
@@ -66,115 +30,20 @@ class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
)
|
||||
)
|
||||
|
||||
def _prepare_credentials(self, **config):
|
||||
if "webhdfs_token" in config:
|
||||
config["token"] = config.pop("webhdfs_token")
|
||||
|
||||
return config
|
||||
|
||||
@wrap_prop(threading.Lock())
|
||||
@cached_property
|
||||
def hdfs_client(self):
|
||||
import hdfs
|
||||
def fs(self):
|
||||
from fsspec.implementations.webhdfs import WebHDFS
|
||||
|
||||
logger.debug("HDFSConfig: %s", self.hdfscli_config)
|
||||
|
||||
try:
|
||||
return hdfs.config.Config(self.hdfscli_config).get_client(
|
||||
self.alias
|
||||
)
|
||||
except hdfs.util.HdfsError as exc:
|
||||
exc_msg = str(exc)
|
||||
errors = (
|
||||
"No alias specified",
|
||||
"Invalid configuration file",
|
||||
f"Alias {self.alias} not found",
|
||||
)
|
||||
if not any(err in exc_msg for err in errors):
|
||||
raise
|
||||
|
||||
http_url = f"http://{self.host}:{self.port}"
|
||||
logger.debug("URL: %s", http_url)
|
||||
|
||||
if self.token is not None:
|
||||
client = hdfs.TokenClient(http_url, token=self.token, root="/")
|
||||
else:
|
||||
client = hdfs.InsecureClient(
|
||||
http_url, user=self.user, root="/"
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
@contextmanager
|
||||
def open(self, path_info, mode="r", encoding=None, **kwargs):
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
|
||||
with self.hdfs_client.read(
|
||||
path_info.path, encoding=encoding
|
||||
) as reader:
|
||||
yield reader
|
||||
|
||||
def walk_files(self, path_info, **kwargs):
|
||||
if not self.exists(path_info):
|
||||
return
|
||||
|
||||
root = path_info.path
|
||||
for path, _, fnames in self.hdfs_client.walk(root):
|
||||
for fname in fnames:
|
||||
yield path_info.replace(path=posixpath.join(path, fname))
|
||||
|
||||
def remove(self, path_info):
|
||||
if path_info.scheme != self.scheme:
|
||||
raise NotImplementedError
|
||||
|
||||
self.hdfs_client.delete(path_info.path)
|
||||
|
||||
def exists(self, path_info) -> bool:
|
||||
assert not isinstance(path_info, list)
|
||||
assert path_info.scheme == "webhdfs"
|
||||
|
||||
status = self.hdfs_client.status(path_info.path, strict=False)
|
||||
return status is not None
|
||||
|
||||
def info(self, path_info):
|
||||
st = self.hdfs_client.status(path_info.path)
|
||||
return {"size": st["length"], "type": "file"}
|
||||
return WebHDFS(**self.fs_args)
|
||||
|
||||
def checksum(self, path_info):
|
||||
return HashInfo(
|
||||
"checksum",
|
||||
self.hdfs_client.checksum(path_info.path)["bytes"],
|
||||
size=self.hdfs_client.status(path_info.path)["length"],
|
||||
)
|
||||
|
||||
def copy(self, from_info, to_info, **_kwargs):
|
||||
with self.hdfs_client.read(from_info.path) as reader:
|
||||
with self.hdfs_client.write(to_info.path) as writer:
|
||||
shutil.copyfileobj(reader, writer)
|
||||
|
||||
def move(self, from_info, to_info):
|
||||
self.hdfs_client.makedirs(to_info.parent.path)
|
||||
self.hdfs_client.rename(from_info.path, to_info.path)
|
||||
|
||||
def upload_fobj(self, fobj, to_info, **kwargs):
|
||||
with self.hdfs_client.write(to_info.path) as fdest:
|
||||
shutil.copyfileobj(fobj, fdest)
|
||||
|
||||
def put_file(
|
||||
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
total = os.path.getsize(from_file)
|
||||
callback.set_size(total)
|
||||
|
||||
self.hdfs_client.makedirs(to_info.parent.path)
|
||||
return self.hdfs_client.upload(
|
||||
to_info.path,
|
||||
from_file,
|
||||
overwrite=True,
|
||||
progress=update_callback(callback, total),
|
||||
)
|
||||
|
||||
def get_file(
|
||||
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
|
||||
):
|
||||
total = self.getsize(from_info)
|
||||
if total:
|
||||
callback.set_size(total)
|
||||
|
||||
self.hdfs_client.download(
|
||||
from_info.path, to_file, progress=update_callback(callback, total)
|
||||
)
|
||||
path = self._with_bucket(path_info)
|
||||
ukey = self.fs.ukey(path)
|
||||
return ukey["bytes"]
|
||||
|
||||
+1
-2
@@ -11,7 +11,6 @@ from dvc.fs.base import BaseFileSystem
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||
from dvc.scheme import Schemes
|
||||
from dvc.system import System
|
||||
from dvc.types import AnyPath, List, Optional
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.collections import PathStringTrie
|
||||
@@ -84,7 +83,7 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
else:
|
||||
return False
|
||||
|
||||
if not System.is_unix():
|
||||
if os.name == "nt":
|
||||
path = normalize_file(path)
|
||||
return path
|
||||
|
||||
|
||||
+8
-5
@@ -136,11 +136,14 @@ def _get_supported_remotes():
|
||||
|
||||
|
||||
def get_fs_type(path):
|
||||
|
||||
partition = {
|
||||
pathlib.Path(part.mountpoint): (part.fstype + " on " + part.device)
|
||||
for part in psutil.disk_partitions(all=True)
|
||||
}
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype != "":
|
||||
try:
|
||||
mountpoint = pathlib.Path(part.mountpoint).resolve()
|
||||
partition[mountpoint] = part.fstype + " on " + part.device
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
# need to follow the symlink: https://github.com/iterative/dvc/issues/5065
|
||||
path = pathlib.Path(path).resolve()
|
||||
|
||||
@@ -142,9 +142,22 @@ class LoggerHandler(logging.StreamHandler):
|
||||
super().handleError(record)
|
||||
raise LoggingException(record)
|
||||
|
||||
def emit_pretty_exception(self, exc, verbose: bool = False):
|
||||
return exc.__pretty_exc__(verbose=verbose)
|
||||
|
||||
def emit(self, record):
|
||||
"""Write to Tqdm's stream so as to not break progress-bars"""
|
||||
try:
|
||||
if record.exc_info:
|
||||
_, exc, *_ = record.exc_info
|
||||
if hasattr(exc, "__pretty_exc__"):
|
||||
try:
|
||||
self.emit_pretty_exception(exc, verbose=_is_verbose())
|
||||
if not _is_verbose():
|
||||
return
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
pass
|
||||
|
||||
msg = self.format(record)
|
||||
Tqdm.write(
|
||||
msg, file=self.stream, end=getattr(self, "terminator", "\n")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from itertools import chain
|
||||
|
||||
from shortuuid import uuid
|
||||
|
||||
@@ -10,52 +11,19 @@ from dvc.exceptions import (
|
||||
DvcException,
|
||||
)
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.objects import check, load
|
||||
from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
|
||||
slow_link_guard,
|
||||
)
|
||||
from dvc.objects.errors import ObjectFormatError
|
||||
from dvc.objects.diff import ROOT
|
||||
from dvc.objects.diff import diff as odiff
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.objects.tree import Tree
|
||||
from dvc.types import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _changed(path_info, fs, obj, cache, state=None):
|
||||
logger.trace("checking if '%s'('%s') has changed.", path_info, obj)
|
||||
|
||||
try:
|
||||
check(cache, obj)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
logger.debug(
|
||||
"cache for '%s'('%s') has changed.", path_info, obj.hash_info
|
||||
)
|
||||
return True
|
||||
|
||||
actual = state.get(path_info, fs) if state else None
|
||||
if actual is None:
|
||||
try:
|
||||
actual = stage(
|
||||
cache, path_info, fs, obj.hash_info.name, dry_run=True
|
||||
)[1].hash_info
|
||||
except FileNotFoundError:
|
||||
logger.debug("'%s' doesn't exist.", path_info)
|
||||
return True
|
||||
|
||||
if obj.hash_info != actual:
|
||||
logger.debug(
|
||||
"hash value '%s' for '%s' has changed (actual '%s').",
|
||||
obj.hash_info,
|
||||
actual,
|
||||
path_info,
|
||||
)
|
||||
return True
|
||||
|
||||
logger.trace("'%s' hasn't changed.", path_info)
|
||||
return False
|
||||
|
||||
|
||||
def _remove(path_info, fs, cache, force=False):
|
||||
def _remove(path_info, fs, in_cache, force=False):
|
||||
if not fs.exists(path_info):
|
||||
return
|
||||
|
||||
@@ -63,13 +31,7 @@ def _remove(path_info, fs, cache, force=False):
|
||||
fs.remove(path_info)
|
||||
return
|
||||
|
||||
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM, dry_run=True)[
|
||||
1
|
||||
].hash_info
|
||||
try:
|
||||
obj = load(cache, current)
|
||||
check(cache, obj)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
if not in_cache:
|
||||
msg = (
|
||||
f"file/directory '{path_info}' is going to be removed. "
|
||||
"Are you sure you want to proceed?"
|
||||
@@ -98,7 +60,7 @@ def _verify_link(cache, path_info, link_type):
|
||||
|
||||
def _do_link(cache, from_info, to_info, link_method):
|
||||
if cache.fs.exists(to_info):
|
||||
raise DvcException(f"Link '{to_info}' already exists!")
|
||||
cache.fs.remove(to_info) # broken symlink
|
||||
|
||||
link_method(from_info, to_info)
|
||||
|
||||
@@ -130,7 +92,7 @@ def _link(cache, from_info, to_info):
|
||||
try:
|
||||
_try_links(cache, from_info, to_info, cache.cache_types)
|
||||
except FileNotFoundError as exc:
|
||||
raise CheckoutError([to_info]) from exc
|
||||
raise CheckoutError([str(to_info)]) from exc
|
||||
|
||||
|
||||
def _cache_is_copy(cache, path_info):
|
||||
@@ -160,7 +122,7 @@ def _cache_is_copy(cache, path_info):
|
||||
def _checkout_file(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
change,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
@@ -169,24 +131,24 @@ def _checkout_file(
|
||||
):
|
||||
"""The file is changed we need to checkout a new copy"""
|
||||
modified = False
|
||||
cache_info = cache.hash_to_path_info(obj.hash_info.value)
|
||||
if fs.exists(path_info):
|
||||
if not relink and _changed(path_info, fs, obj, cache, state=state):
|
||||
modified = True
|
||||
_remove(path_info, fs, cache, force=force)
|
||||
_link(cache, cache_info, path_info)
|
||||
else:
|
||||
cache_info = cache.hash_to_path_info(change.new.obj.hash_info.value)
|
||||
if change.old.obj:
|
||||
if relink:
|
||||
if fs.iscopy(path_info) and _cache_is_copy(cache, path_info):
|
||||
cache.unprotect(path_info)
|
||||
else:
|
||||
_remove(path_info, fs, cache, force=force)
|
||||
_remove(path_info, fs, change.old.in_cache, force=force)
|
||||
_link(cache, cache_info, path_info)
|
||||
else:
|
||||
modified = True
|
||||
_remove(path_info, fs, change.old.in_cache, force=force)
|
||||
_link(cache, cache_info, path_info)
|
||||
else:
|
||||
_link(cache, cache_info, path_info)
|
||||
modified = True
|
||||
|
||||
if state:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
state.save(path_info, fs, change.new.obj.hash_info)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(str(path_info))
|
||||
@@ -194,109 +156,83 @@ def _checkout_file(
|
||||
return modified
|
||||
|
||||
|
||||
def _remove_redundant_files(
|
||||
path_info, fs, obj, cache, force, dvcignore: Optional[DvcIgnoreFilter]
|
||||
):
|
||||
if dvcignore:
|
||||
existing_files = set(dvcignore.walk_files(fs, path_info))
|
||||
else:
|
||||
existing_files = set(fs.walk_files(path_info))
|
||||
|
||||
needed_files = {path_info.joinpath(*key) for key, _ in obj}
|
||||
redundant_files = existing_files - needed_files
|
||||
for path in redundant_files:
|
||||
_remove(path, fs, cache, force)
|
||||
|
||||
return bool(redundant_files)
|
||||
|
||||
|
||||
def _checkout_dir(
|
||||
def _diff(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
modified = False
|
||||
# Create dir separately so that dir is created
|
||||
# even if there are no files in it
|
||||
if not fs.exists(path_info):
|
||||
modified = True
|
||||
fs.makedirs(path_info)
|
||||
|
||||
logger.debug("Linking directory '%s'.", path_info)
|
||||
|
||||
failed = []
|
||||
for entry_key, entry_obj in obj:
|
||||
try:
|
||||
entry_modified = _checkout_file(
|
||||
path_info.joinpath(*entry_key),
|
||||
fs,
|
||||
entry_obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
state=None,
|
||||
)
|
||||
if entry_modified:
|
||||
modified = True
|
||||
except CheckoutError as exc:
|
||||
failed.extend(exc.target_infos)
|
||||
|
||||
modified = (
|
||||
_remove_redundant_files(
|
||||
path_info, fs, obj, cache, force, dvcignore=dvcignore
|
||||
old = None
|
||||
try:
|
||||
_, old = stage(
|
||||
cache,
|
||||
path_info,
|
||||
fs,
|
||||
obj.hash_info.name if obj else cache.fs.PARAM_CHECKSUM,
|
||||
dry_run=True,
|
||||
dvcignore=dvcignore,
|
||||
)
|
||||
or modified
|
||||
)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
if failed:
|
||||
raise CheckoutError(failed)
|
||||
diff = odiff(old, obj, cache)
|
||||
|
||||
if state:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
if relink:
|
||||
diff.modified.extend(diff.unchanged)
|
||||
|
||||
# relink is not modified, assume it as nochange
|
||||
return modified and not relink
|
||||
return diff
|
||||
|
||||
|
||||
def _checkout(
|
||||
diff,
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
relink=False,
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
if not obj.hash_info.isdir:
|
||||
ret = _checkout_file(
|
||||
path_info, fs, obj, cache, force, progress_callback, relink, state
|
||||
)
|
||||
else:
|
||||
ret = _checkout_dir(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
dvcignore=dvcignore,
|
||||
state=state,
|
||||
)
|
||||
if not diff:
|
||||
return
|
||||
|
||||
if state:
|
||||
state.save_link(path_info, fs)
|
||||
for change in diff.deleted:
|
||||
entry_path = (
|
||||
path_info.joinpath(*change.old.key)
|
||||
if change.old.key != ROOT
|
||||
else path_info
|
||||
)
|
||||
_remove(entry_path, fs, change.old.in_cache, force=force)
|
||||
|
||||
return ret
|
||||
failed = []
|
||||
for change in chain(diff.added, diff.modified):
|
||||
entry_path = (
|
||||
path_info.joinpath(*change.new.key)
|
||||
if change.new.key != ROOT
|
||||
else path_info
|
||||
)
|
||||
if isinstance(change.new.obj, Tree):
|
||||
fs.makedirs(entry_path)
|
||||
continue
|
||||
|
||||
try:
|
||||
_checkout_file(
|
||||
entry_path,
|
||||
fs,
|
||||
change,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
state=state,
|
||||
)
|
||||
except CheckoutError as exc:
|
||||
failed.extend(exc.target_infos)
|
||||
|
||||
if failed:
|
||||
raise CheckoutError(failed)
|
||||
|
||||
|
||||
def checkout(
|
||||
@@ -311,41 +247,54 @@ def checkout(
|
||||
dvcignore: Optional[DvcIgnoreFilter] = None,
|
||||
state=None,
|
||||
):
|
||||
|
||||
if path_info.scheme not in ["local", cache.fs.scheme]:
|
||||
raise NotImplementedError
|
||||
|
||||
failed = None
|
||||
skip = False
|
||||
diff = _diff(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
relink=relink,
|
||||
dvcignore=dvcignore,
|
||||
)
|
||||
|
||||
failed = []
|
||||
if not obj:
|
||||
if not quiet:
|
||||
logger.warning(
|
||||
"No file hash info found for '%s'. It won't be created.",
|
||||
path_info,
|
||||
)
|
||||
_remove(path_info, fs, cache, force=force)
|
||||
failed = path_info
|
||||
|
||||
elif not relink and not _changed(path_info, fs, obj, cache, state=state):
|
||||
failed.append(str(path_info))
|
||||
elif not diff:
|
||||
logger.trace("Data '%s' didn't change.", path_info) # type: ignore
|
||||
skip = True
|
||||
|
||||
if failed or skip:
|
||||
try:
|
||||
_checkout(
|
||||
diff,
|
||||
path_info,
|
||||
fs,
|
||||
cache,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
relink=relink,
|
||||
state=state,
|
||||
)
|
||||
except CheckoutError as exc:
|
||||
failed.extend(exc.target_infos)
|
||||
|
||||
if diff and state:
|
||||
state.save_link(path_info, fs)
|
||||
if not failed:
|
||||
state.save(path_info, fs, obj.hash_info)
|
||||
|
||||
if failed or not diff:
|
||||
if progress_callback and obj:
|
||||
progress_callback(str(path_info), len(obj))
|
||||
if failed:
|
||||
raise CheckoutError([failed])
|
||||
raise CheckoutError(failed)
|
||||
return
|
||||
|
||||
logger.debug("Checking out '%s' with cache '%s'.", path_info, obj)
|
||||
|
||||
return _checkout(
|
||||
path_info,
|
||||
fs,
|
||||
obj,
|
||||
cache,
|
||||
force,
|
||||
progress_callback,
|
||||
relink,
|
||||
dvcignore=dvcignore,
|
||||
state=state,
|
||||
)
|
||||
return bool(diff) and not relink
|
||||
@@ -0,0 +1,135 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .file import HashFile
|
||||
|
||||
ADD = "add"
|
||||
MODIFY = "modify"
|
||||
DELETE = "delete"
|
||||
UNCHANGED = "unchanged"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TreeEntry:
|
||||
in_cache: bool
|
||||
key: Tuple[str]
|
||||
obj: Optional["HashFile"] = field(default=None)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.obj)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, TreeEntry):
|
||||
return False
|
||||
|
||||
if self.key != other.key or bool(self.obj) != bool(other.obj):
|
||||
return False
|
||||
|
||||
if not self.obj:
|
||||
return False
|
||||
|
||||
return self.obj.hash_info == other.obj.hash_info
|
||||
|
||||
|
||||
@dataclass
|
||||
class Change:
|
||||
old: TreeEntry
|
||||
new: TreeEntry
|
||||
|
||||
@property
|
||||
def typ(self):
|
||||
if not self.old and not self.new:
|
||||
return UNCHANGED
|
||||
|
||||
if self.old and not self.new:
|
||||
return DELETE
|
||||
|
||||
if not self.old and self.new:
|
||||
return ADD
|
||||
|
||||
if self.old != self.new:
|
||||
return MODIFY
|
||||
|
||||
return UNCHANGED
|
||||
|
||||
def __bool__(self):
|
||||
return self.typ != UNCHANGED
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiffResult:
|
||||
added: List[Change] = field(default_factory=list, compare=True)
|
||||
modified: List[Change] = field(default_factory=list, compare=True)
|
||||
deleted: List[Change] = field(default_factory=list, compare=True)
|
||||
unchanged: List[Change] = field(default_factory=list, compare=True)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.added or self.modified or self.deleted)
|
||||
|
||||
|
||||
ROOT = ("",)
|
||||
|
||||
|
||||
def diff(
|
||||
old: Optional["HashFile"], new: Optional["HashFile"], cache
|
||||
) -> DiffResult:
|
||||
from .tree import Tree
|
||||
|
||||
if old is None and new is None:
|
||||
return DiffResult()
|
||||
|
||||
def _get_keys(obj):
|
||||
if not obj:
|
||||
return []
|
||||
return [ROOT] + (
|
||||
[key for key, _ in obj] if isinstance(obj, Tree) else []
|
||||
)
|
||||
|
||||
old_keys = set(_get_keys(old))
|
||||
new_keys = set(_get_keys(new))
|
||||
|
||||
def _get_obj(obj, key):
|
||||
if not obj or key == ROOT:
|
||||
return obj
|
||||
|
||||
return obj.get(key)
|
||||
|
||||
def _in_cache(obj, cache):
|
||||
from . import check
|
||||
from .errors import ObjectFormatError
|
||||
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
try:
|
||||
check(cache, obj)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
|
||||
ret = DiffResult()
|
||||
for key in old_keys | new_keys:
|
||||
old_obj = _get_obj(old, key)
|
||||
new_obj = _get_obj(new, key)
|
||||
|
||||
change = Change(
|
||||
old=TreeEntry(_in_cache(old_obj, cache), key, old_obj),
|
||||
new=TreeEntry(_in_cache(new_obj, cache), key, new_obj),
|
||||
)
|
||||
|
||||
if change.typ == ADD:
|
||||
ret.added.append(change)
|
||||
elif change.typ == MODIFY:
|
||||
ret.modified.append(change)
|
||||
elif change.typ == DELETE:
|
||||
ret.deleted.append(change)
|
||||
else:
|
||||
assert change.typ == UNCHANGED
|
||||
if not change.new.in_cache and not isinstance(
|
||||
change.new.obj, Tree
|
||||
):
|
||||
ret.modified.append(change)
|
||||
else:
|
||||
ret.unchanged.append(change)
|
||||
return ret
|
||||
+9
-11
@@ -45,18 +45,16 @@ def _get_file_hash(path_info, fs, name):
|
||||
info = fs.info(path_info)
|
||||
if name in info:
|
||||
assert not info[name].endswith(".dir")
|
||||
return HashInfo(name, info[name], size=info["size"])
|
||||
hash_value = info[name]
|
||||
elif hasattr(fs, name):
|
||||
func = getattr(fs, name)
|
||||
hash_value = func(path_info)
|
||||
elif name == "md5":
|
||||
hash_value = file_md5(path_info, fs)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
func = getattr(fs, name, None)
|
||||
if func:
|
||||
return func(path_info)
|
||||
|
||||
if name == "md5":
|
||||
return HashInfo(
|
||||
name, file_md5(path_info, fs), size=fs.getsize(path_info)
|
||||
)
|
||||
|
||||
raise NotImplementedError
|
||||
return HashInfo(name, hash_value, size=info["size"])
|
||||
|
||||
|
||||
def get_file_hash(path_info, fs, name, state=None):
|
||||
|
||||
+1
-1
@@ -9,7 +9,6 @@ from funcy import collecting, project
|
||||
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
|
||||
|
||||
from dvc import objects, prompt
|
||||
from dvc.checkout import checkout
|
||||
from dvc.exceptions import (
|
||||
CheckoutError,
|
||||
CollectCacheError,
|
||||
@@ -17,6 +16,7 @@ from dvc.exceptions import (
|
||||
MergeError,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc.objects.checkout import checkout
|
||||
|
||||
from .fs import get_cloud_fs
|
||||
from .fs.hdfs import HDFSFileSystem
|
||||
|
||||
+7
-13
@@ -136,8 +136,8 @@ class Tqdm(tqdm):
|
||||
|
||||
return wrapped
|
||||
|
||||
def as_callback(self, fs, path_info):
|
||||
return FsspecCallback(fs, path_info, self)
|
||||
def as_callback(self):
|
||||
return FsspecCallback(self)
|
||||
|
||||
def close(self):
|
||||
self.postfix["info"] = ""
|
||||
@@ -167,21 +167,15 @@ class Tqdm(tqdm):
|
||||
|
||||
|
||||
class FsspecCallback(fsspec.Callback):
|
||||
def __init__(self, fs, path_info, progress_bar):
|
||||
self.fs = fs
|
||||
self.path_info = path_info
|
||||
def __init__(self, progress_bar):
|
||||
self.progress_bar = progress_bar
|
||||
super().__init__()
|
||||
|
||||
def set_size(self, size):
|
||||
"""``set_size()`` is an API that might be called with ``None`` if the
|
||||
information is not already present on the caller. In that case, we'll
|
||||
retrieve the size from an ``info()`` call."""
|
||||
if size is None:
|
||||
size = self.fs.info(self.path_info)["size"]
|
||||
self.progress_bar.total = size
|
||||
self.progress_bar.refresh()
|
||||
super().set_size(size)
|
||||
if size is not None:
|
||||
self.progress_bar.total = size
|
||||
self.progress_bar.refresh()
|
||||
super().set_size(size)
|
||||
|
||||
def relative_update(self, inc=1):
|
||||
self.progress_bar.update(inc)
|
||||
|
||||
+1
-2
@@ -30,7 +30,6 @@ class ImageRenderer(Renderer):
|
||||
img_path = os.path.join(
|
||||
static, f"{revision}_{filename.replace(os.sep, '_')}"
|
||||
)
|
||||
rel_img_path = relpath(img_path, page_dir_path)
|
||||
with open(img_path, "wb") as fd:
|
||||
fd.write(image_data)
|
||||
return """
|
||||
@@ -38,7 +37,7 @@ class ImageRenderer(Renderer):
|
||||
<p>{title}</p>
|
||||
<img src="{src}">
|
||||
</div>""".format(
|
||||
title=revision, src=rel_img_path
|
||||
title=revision, src=(relpath(img_path, page_dir_path))
|
||||
)
|
||||
|
||||
def _convert(self, path: "StrPath"):
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ class PlotDataStructureError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Plot data extraction failed. Please see "
|
||||
"https://man.dvc.org/plot for supported data formats."
|
||||
"https://man.dvc.org/plots for supported data formats."
|
||||
)
|
||||
|
||||
|
||||
|
||||
+34
-4
@@ -120,6 +120,34 @@ class Repo:
|
||||
|
||||
return root_dir, dvc_dir, tmp_dir
|
||||
|
||||
def _get_database_dir(self, db_name):
|
||||
# NOTE: by default, store SQLite-based remote indexes and state's
|
||||
# `links` and `md5s` caches in the repository itself to avoid any
|
||||
# possible state corruption in 'shared cache dir' scenario, but allow
|
||||
# user to override this through config when, say, the repository is
|
||||
# located on a mounted volume — see
|
||||
# https://github.com/iterative/dvc/issues/4420
|
||||
base_db_dir = self.config.get(db_name, {}).get("dir", None)
|
||||
if not base_db_dir:
|
||||
return self.tmp_dir
|
||||
|
||||
import hashlib
|
||||
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
root_dir_hash = hashlib.sha224(
|
||||
self.root_dir.encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
db_dir = os.path.join(
|
||||
base_db_dir,
|
||||
self.DVC_DIR,
|
||||
f"{os.path.basename(self.root_dir)}-{root_dir_hash[0:7]}",
|
||||
)
|
||||
|
||||
makedirs(db_dir, exist_ok=True)
|
||||
return db_dir
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_dir=None,
|
||||
@@ -183,10 +211,8 @@ class Repo:
|
||||
friendly=True,
|
||||
)
|
||||
|
||||
# NOTE: storing state and link_state in the repository itself to
|
||||
# avoid any possible state corruption in 'shared cache dir'
|
||||
# scenario.
|
||||
self.state = State(self.root_dir, self.tmp_dir, self.dvcignore)
|
||||
state_db_dir = self._get_database_dir("state")
|
||||
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
|
||||
self.odb = ODBManager(self)
|
||||
|
||||
self.stage_cache = StageCache(self)
|
||||
@@ -452,6 +478,10 @@ class Repo:
|
||||
|
||||
return RepoFileSystem(self, subrepos=self.subrepos, **self._fs_conf)
|
||||
|
||||
@cached_property
|
||||
def index_db_dir(self):
|
||||
return self._get_database_dir("index")
|
||||
|
||||
@contextmanager
|
||||
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
||||
"""Opens a specified resource as a file descriptor"""
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, Iterable
|
||||
|
||||
from funcy import compact, lremove, post_processing
|
||||
from rich.prompt import Prompt
|
||||
|
||||
from dvc.types import OptStr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.dvcfile import DVCFile
|
||||
from dvc.stage import Stage
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
PROMPTS = {
|
||||
"cmd": "[b]Command[/b] to execute",
|
||||
"code": "Path to a [b]code[/b] file/directory",
|
||||
"data": "Path to a [b]data[/b] file/directory",
|
||||
"models": "Path to a [b]model[/b] file/directory",
|
||||
"params": "Path to a [b]parameters[/b] file",
|
||||
"metrics": "Path to a [b]metrics[/b] file",
|
||||
"plots": "Path to a [b]plots[/b] file/directory",
|
||||
"live": "Path to log [b]dvclive[/b] outputs",
|
||||
}
|
||||
|
||||
|
||||
class RequiredPrompt(Prompt):
|
||||
def process_response(self, value: str):
|
||||
from rich.prompt import InvalidResponse
|
||||
|
||||
ret = super().process_response(value)
|
||||
if not ret:
|
||||
raise InvalidResponse(
|
||||
"[prompt.invalid]Response required. Please try again."
|
||||
)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def get_input(cls, *args, **kwargs) -> str:
|
||||
try:
|
||||
return super().get_input(*args, **kwargs)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
ui.error_write()
|
||||
raise
|
||||
|
||||
def render_default(self, default):
|
||||
from rich.text import Text
|
||||
|
||||
return Text(f"{default!s}", "green")
|
||||
|
||||
|
||||
class SkippablePrompt(RequiredPrompt):
|
||||
skip_value: str = "n"
|
||||
|
||||
def process_response(self, value: str):
|
||||
ret = super().process_response(value)
|
||||
return None if ret == self.skip_value else ret
|
||||
|
||||
def make_prompt(self, default):
|
||||
prompt = self.prompt.copy()
|
||||
prompt.end = ""
|
||||
|
||||
prompt.append(" [")
|
||||
if (
|
||||
default is not ...
|
||||
and self.show_default
|
||||
and isinstance(default, (str, self.response_type))
|
||||
):
|
||||
_default = self.render_default(default)
|
||||
prompt.append(_default)
|
||||
prompt.append(", ")
|
||||
|
||||
prompt.append(f"{self.skip_value} to skip", style="italic")
|
||||
prompt.append("]")
|
||||
prompt.append(self.prompt_suffix)
|
||||
return prompt
|
||||
|
||||
|
||||
@post_processing(dict)
|
||||
def _prompts(keys: Iterable[str], defaults: Dict[str, OptStr]):
|
||||
for key in keys:
|
||||
if key == "cmd":
|
||||
prompt_cls = RequiredPrompt
|
||||
else:
|
||||
prompt_cls = SkippablePrompt
|
||||
kwargs = {"default": defaults[key]} if key in defaults else {}
|
||||
prompt = PROMPTS.get(key)
|
||||
value = prompt_cls.ask(prompt, console=ui.error_console, **kwargs)
|
||||
yield key, value
|
||||
|
||||
|
||||
def init_interactive(
|
||||
defaults: Dict[str, str],
|
||||
provided: Iterable[str],
|
||||
show_heading: bool = False,
|
||||
live: bool = False,
|
||||
) -> Dict[str, str]:
|
||||
primary = lremove(provided, ["cmd", "code", "data", "models", "params"])
|
||||
secondary = lremove(provided, ["live"] if live else ["metrics", "plots"])
|
||||
|
||||
if not (primary or secondary):
|
||||
return {}
|
||||
|
||||
message = (
|
||||
"This command will guide you to set up your first stage in "
|
||||
"[green]dvc.yaml[/green].\n"
|
||||
)
|
||||
if show_heading:
|
||||
ui.error_write(message, styled=True)
|
||||
|
||||
return compact(
|
||||
{
|
||||
**_prompts(primary, defaults),
|
||||
**_prompts(secondary, defaults),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _check_stage_exists(
|
||||
dvcfile: "DVCFile", name: str, force: bool = False
|
||||
) -> None:
|
||||
if not force and dvcfile.exists() and name in dvcfile.stages:
|
||||
from dvc.stage.exceptions import DuplicateStageName
|
||||
|
||||
hint = "Use '--force' to overwrite."
|
||||
raise DuplicateStageName(
|
||||
f"Stage '{name}' already exists in 'dvc.yaml'. {hint}"
|
||||
)
|
||||
|
||||
|
||||
def init(
|
||||
repo: "Repo",
|
||||
name: str = None,
|
||||
type: str = "default", # pylint: disable=redefined-builtin
|
||||
defaults: Dict[str, str] = None,
|
||||
overrides: Dict[str, str] = None,
|
||||
interactive: bool = False,
|
||||
force: bool = False,
|
||||
) -> "Stage":
|
||||
from dvc.dvcfile import make_dvcfile
|
||||
|
||||
dvcfile = make_dvcfile(repo, "dvc.yaml")
|
||||
name = name or type
|
||||
|
||||
_check_stage_exists(dvcfile, name, force=force)
|
||||
|
||||
defaults = defaults or {}
|
||||
overrides = overrides or {}
|
||||
|
||||
with_live = type == "live"
|
||||
if interactive:
|
||||
defaults = init_interactive(
|
||||
defaults=defaults or {},
|
||||
show_heading=not dvcfile.exists(),
|
||||
live=with_live,
|
||||
provided=overrides.keys(),
|
||||
)
|
||||
else:
|
||||
if with_live:
|
||||
# suppress `metrics`/`params` if live is selected, unless
|
||||
# it is also provided via overrides/cli.
|
||||
# This makes output to be a checkpoint as well.
|
||||
defaults.pop("metrics")
|
||||
defaults.pop("params")
|
||||
else:
|
||||
defaults.pop("live") # suppress live otherwise
|
||||
|
||||
context: Dict[str, str] = {**defaults, **overrides}
|
||||
assert "cmd" in context
|
||||
|
||||
params_kv = []
|
||||
if context.get("params"):
|
||||
from dvc.utils.serialize import LOADERS
|
||||
|
||||
path = context["params"]
|
||||
assert isinstance(path, str)
|
||||
_, ext = os.path.splitext(path)
|
||||
params_kv = [{path: list(LOADERS[ext](path))}]
|
||||
|
||||
checkpoint_out = bool(context.get("live"))
|
||||
models = context.get("models")
|
||||
return repo.stage.add(
|
||||
name=name,
|
||||
cmd=context["cmd"],
|
||||
deps=compact([context.get("code"), context.get("data")]),
|
||||
params=params_kv,
|
||||
metrics_no_cache=compact([context.get("metrics")]),
|
||||
plots_no_cache=compact([context.get("plots")]),
|
||||
live=context.get("live"),
|
||||
force=force,
|
||||
**{"checkpoints" if checkpoint_out else "outs": compact([models])},
|
||||
)
|
||||
+19
-1
@@ -1,3 +1,5 @@
|
||||
from collections.abc import Mapping
|
||||
|
||||
from voluptuous import Any, Optional, Required, Schema
|
||||
|
||||
from dvc import dependency, output
|
||||
@@ -94,11 +96,27 @@ STAGE_DEFINITION = {
|
||||
Optional(StageParams.PARAM_LIVE): Any(str, LIVE_PSTAGE_SCHEMA),
|
||||
}
|
||||
|
||||
|
||||
def either_or(primary, fallback, fallback_includes=None):
|
||||
def validator(data):
|
||||
schema = primary
|
||||
if (
|
||||
isinstance(data, Mapping)
|
||||
and set(fallback_includes or []) & data.keys()
|
||||
):
|
||||
schema = fallback
|
||||
return Schema(schema)(data)
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
FOREACH_IN = {
|
||||
Required(FOREACH_KWD): Any(dict, list, str),
|
||||
Required(DO_KWD): STAGE_DEFINITION,
|
||||
}
|
||||
SINGLE_PIPELINE_STAGE_SCHEMA = {str: Any(FOREACH_IN, STAGE_DEFINITION)}
|
||||
SINGLE_PIPELINE_STAGE_SCHEMA = {
|
||||
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
|
||||
}
|
||||
MULTI_STAGE_SCHEMA = {
|
||||
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
|
||||
VARS_KWD: VARS_SCHEMA,
|
||||
|
||||
@@ -273,7 +273,7 @@ class Stage(params.StageParams):
|
||||
from dvc.output import Output
|
||||
from dvc.schema import LIVE_PROPS
|
||||
|
||||
env[DVCLIVE_PATH] = str(out.path_info)
|
||||
env[DVCLIVE_PATH] = relpath(out.path_info, self.wdir)
|
||||
if isinstance(out.live, dict):
|
||||
config = project(out.live, LIVE_PROPS)
|
||||
|
||||
|
||||
@@ -9,10 +9,6 @@ class StageCmdFailedError(DvcException):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class StageFileFormatError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class StageFileDoesNotExistError(DvcException):
|
||||
DVC_IGNORED = "is dvc-ignored"
|
||||
DOES_NOT_EXIST = "does not exist"
|
||||
|
||||
+7
-18
@@ -79,11 +79,6 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
|
||||
if not isinstance(fs, LocalFileSystem):
|
||||
return
|
||||
|
||||
assert isinstance(path_info, str) or path_info.scheme == "local"
|
||||
assert hash_info
|
||||
assert isinstance(hash_info, HashInfo)
|
||||
assert os.path.exists(path_info)
|
||||
|
||||
mtime, size = get_mtime_and_size(path_info, fs, self.dvcignore)
|
||||
inode = get_inode(path_info)
|
||||
|
||||
@@ -111,17 +106,12 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
|
||||
if not isinstance(fs, LocalFileSystem):
|
||||
return None
|
||||
|
||||
assert isinstance(path_info, str) or path_info.scheme == "local"
|
||||
path = os.fspath(path_info)
|
||||
|
||||
# NOTE: use os.path.exists instead of LocalFileSystem.exists
|
||||
# because it uses lexists() and will return True for broken
|
||||
# symlinks that we cannot stat() in get_mtime_and_size
|
||||
if not os.path.exists(path):
|
||||
try:
|
||||
mtime, size = get_mtime_and_size(path_info, fs, self.dvcignore)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
mtime, size = get_mtime_and_size(path, fs, self.dvcignore)
|
||||
inode = get_inode(path)
|
||||
inode = get_inode(path_info)
|
||||
|
||||
value = self.md5s.get(inode)
|
||||
|
||||
@@ -140,12 +130,11 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
|
||||
if not isinstance(fs, LocalFileSystem):
|
||||
return
|
||||
|
||||
assert isinstance(path_info, str) or path_info.scheme == "local"
|
||||
|
||||
if not fs.exists(path_info):
|
||||
try:
|
||||
mtime, _ = get_mtime_and_size(path_info, fs, self.dvcignore)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
|
||||
mtime, _ = get_mtime_and_size(path_info, fs, self.dvcignore)
|
||||
inode = get_inode(path_info)
|
||||
relative_path = relpath(path_info, self.root_dir)
|
||||
|
||||
|
||||
+3
-105
@@ -2,30 +2,13 @@ import errno
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if (
|
||||
sys.platform == "win32"
|
||||
and sys.version_info < (3, 8)
|
||||
and sys.getwindowsversion() >= (6, 2)
|
||||
):
|
||||
try:
|
||||
import speedcopy
|
||||
|
||||
speedcopy.patch_copyfile()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class System:
|
||||
@staticmethod
|
||||
def is_unix():
|
||||
return os.name != "nt"
|
||||
|
||||
@staticmethod
|
||||
def hardlink(source, link_name):
|
||||
try:
|
||||
@@ -117,99 +100,14 @@ class System:
|
||||
# so we need to chmod it to look like a normal copy.
|
||||
os.chmod(link_name, 0o666 & ~umask)
|
||||
|
||||
@staticmethod
|
||||
def _getdirinfo(path):
|
||||
from collections import namedtuple
|
||||
|
||||
from win32file import ( # pylint: disable=import-error
|
||||
FILE_FLAG_BACKUP_SEMANTICS,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
FILE_SHARE_READ,
|
||||
OPEN_EXISTING,
|
||||
CreateFileW,
|
||||
GetFileInformationByHandle,
|
||||
)
|
||||
|
||||
# NOTE: use FILE_FLAG_OPEN_REPARSE_POINT to open symlink itself and not
|
||||
# the target See https://docs.microsoft.com/en-us/windows/desktop/api/
|
||||
# fileapi/nf-fileapi-createfilew#symbolic-link-behavior
|
||||
flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT
|
||||
|
||||
hfile = CreateFileW(
|
||||
path, 0, FILE_SHARE_READ, None, OPEN_EXISTING, flags, None
|
||||
)
|
||||
|
||||
# See BY_HANDLE_FILE_INFORMATION structure from fileapi.h
|
||||
Info = namedtuple(
|
||||
"BY_HANDLE_FILE_INFORMATION",
|
||||
[
|
||||
"dwFileAttributes",
|
||||
"ftCreationTime",
|
||||
"ftLastAccessTime",
|
||||
"ftLastWriteTime",
|
||||
"dwVolumeSerialNumber",
|
||||
"nFileSizeHigh",
|
||||
"nFileSizeLow",
|
||||
"nNumberOfLinks",
|
||||
"nFileIndexHigh",
|
||||
"nFileIndexLow",
|
||||
],
|
||||
)
|
||||
|
||||
return Info(*GetFileInformationByHandle(hfile))
|
||||
|
||||
@staticmethod
|
||||
def inode(path):
|
||||
path = os.fspath(path)
|
||||
|
||||
if System.is_unix():
|
||||
import ctypes
|
||||
|
||||
inode = os.lstat(path).st_ino
|
||||
# NOTE: See https://bugs.python.org/issue29619 and
|
||||
# https://stackoverflow.com/questions/34643289/
|
||||
# pythons-os-stat-is-returning-wrong-inode-value
|
||||
inode = ctypes.c_ulong(inode).value
|
||||
else:
|
||||
# getdirinfo from ntfsutils works on both files and dirs
|
||||
info = System._getdirinfo(path)
|
||||
inode = abs(
|
||||
hash(
|
||||
(
|
||||
info.dwVolumeSerialNumber,
|
||||
info.nFileIndexHigh,
|
||||
info.nFileIndexLow,
|
||||
)
|
||||
)
|
||||
)
|
||||
assert inode >= 0
|
||||
assert inode < 2 ** 64
|
||||
return inode
|
||||
return os.lstat(path).st_ino
|
||||
|
||||
@staticmethod
|
||||
def is_symlink(path):
|
||||
path = os.fspath(path)
|
||||
|
||||
if System.is_unix():
|
||||
return os.path.islink(path)
|
||||
|
||||
# https://docs.microsoft.com/en-us/windows/desktop/fileio/
|
||||
# file-attribute-constants
|
||||
from winnt import ( # pylint: disable=import-error
|
||||
FILE_ATTRIBUTE_REPARSE_POINT,
|
||||
)
|
||||
|
||||
if os.path.lexists(path):
|
||||
info = System._getdirinfo(path)
|
||||
return info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT
|
||||
return False
|
||||
return os.path.islink(path)
|
||||
|
||||
@staticmethod
|
||||
def is_hardlink(path):
|
||||
path = os.fspath(path)
|
||||
|
||||
if System.is_unix():
|
||||
return os.stat(path).st_nlink > 1
|
||||
|
||||
info = System._getdirinfo(path)
|
||||
return info.nNumberOfLinks > 1
|
||||
return os.stat(path).st_nlink > 1
|
||||
|
||||
+57
-6
@@ -1,12 +1,15 @@
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Optional,
|
||||
Sequence,
|
||||
TextIO,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
@@ -14,6 +17,7 @@ from funcy import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.status import Status
|
||||
from rich.text import Text as RichText
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.ui.table import Headers, Styles, TableData
|
||||
@@ -40,8 +44,13 @@ class Console:
|
||||
def __init__(
|
||||
self, formatter: Formatter = None, enable: bool = False
|
||||
) -> None:
|
||||
from contextvars import ContextVar
|
||||
|
||||
self.formatter: Formatter = formatter or Formatter()
|
||||
self._enabled: bool = enable
|
||||
self._paginate: ContextVar[bool] = ContextVar(
|
||||
"_paginate", default=False
|
||||
)
|
||||
|
||||
def enable(self) -> None:
|
||||
self._enabled = True
|
||||
@@ -74,6 +83,23 @@ class Console:
|
||||
styled=styled,
|
||||
)
|
||||
|
||||
def write_json(
|
||||
self, data: Any, indent: int = 2, default: Callable = None
|
||||
) -> None:
|
||||
import json
|
||||
|
||||
if self.isatty():
|
||||
from rich.highlighter import JSONHighlighter
|
||||
|
||||
j = json.dumps(data, indent=indent, default=default)
|
||||
highlighter = JSONHighlighter()
|
||||
text = highlighter(j)
|
||||
text.no_wrap = True
|
||||
text.overflow = None
|
||||
return self.write(text, styled=True)
|
||||
|
||||
return self.write(json.dumps(data))
|
||||
|
||||
def write(
|
||||
self,
|
||||
*objects: Any,
|
||||
@@ -85,6 +111,8 @@ class Console:
|
||||
styled: bool = False,
|
||||
file: TextIO = None,
|
||||
) -> None:
|
||||
import sys
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
sep = " " if sep is None else sep
|
||||
@@ -94,21 +122,39 @@ class Console:
|
||||
|
||||
file = file or (sys.stderr if stderr else sys.stdout)
|
||||
with Tqdm.external_write_mode(file=file):
|
||||
if styled:
|
||||
# if we are inside pager context, send the output to rich's buffer
|
||||
if styled or self._paginate.get():
|
||||
console = self.error_console if stderr else self.rich_console
|
||||
return console.print(*objects, sep=sep, end=end)
|
||||
if styled:
|
||||
return console.print(*objects, sep=sep, end=end)
|
||||
return console.out(*objects, sep=sep, end=end, highlight=False)
|
||||
|
||||
values = (
|
||||
self.formatter.format(obj, style=style) for obj in objects
|
||||
)
|
||||
values = (self.formatter.format(obj, style) for obj in objects)
|
||||
return print(*values, sep=sep, end=end, file=file)
|
||||
|
||||
@property
|
||||
def rich_text(self) -> "Type[RichText]":
|
||||
from rich.text import Text
|
||||
|
||||
return Text
|
||||
|
||||
@staticmethod
|
||||
def progress(*args, **kwargs) -> "Tqdm":
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
return Tqdm(*args, **kwargs)
|
||||
|
||||
@contextmanager
|
||||
def pager(self, styles: bool = True) -> Iterator[None]:
|
||||
from .pager import DvcPager
|
||||
|
||||
tok = self._paginate.set(True)
|
||||
try:
|
||||
with self.rich_console.pager(pager=DvcPager(), styles=styles):
|
||||
yield
|
||||
finally:
|
||||
self._paginate.reset(tok)
|
||||
|
||||
def prompt(
|
||||
self, text: str, choices: Iterable[str] = None, password: bool = False
|
||||
) -> Optional[str]:
|
||||
@@ -195,6 +241,11 @@ class Console:
|
||||
def status(self, status: str, **kwargs: Any) -> "Status":
|
||||
return self.error_console.status(status, **kwargs)
|
||||
|
||||
def isatty(self) -> bool:
|
||||
import sys
|
||||
|
||||
return sys.stdout.isatty()
|
||||
|
||||
|
||||
ui = Console()
|
||||
|
||||
|
||||
+14
-19
@@ -1,5 +1,5 @@
|
||||
from collections import abc
|
||||
from contextlib import contextmanager
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from itertools import zip_longest
|
||||
from typing import TYPE_CHECKING, Dict, Iterator, Sequence, Union
|
||||
|
||||
@@ -8,15 +8,13 @@ from dvc.types import DictStrAny
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.ui import Console
|
||||
|
||||
from dvc.ui import Console, RichText
|
||||
|
||||
SHOW_MAX_WIDTH = 1024
|
||||
|
||||
|
||||
CellT = Union[str, "Text"] # Text is mostly compatible with str
|
||||
CellT = Union[str, "RichText"] # RichText is mostly compatible with str
|
||||
Row = Sequence[CellT]
|
||||
TableData = Sequence[Row]
|
||||
Headers = Sequence[str]
|
||||
@@ -31,6 +29,7 @@ def plain_table(
|
||||
pager: bool = False,
|
||||
force: bool = True,
|
||||
) -> None:
|
||||
from funcy import nullcontext
|
||||
from tabulate import tabulate
|
||||
|
||||
text: str = tabulate(
|
||||
@@ -45,11 +44,8 @@ def plain_table(
|
||||
# NOTE: md table is incomplete without the trailing newline
|
||||
text += "\n"
|
||||
|
||||
if pager:
|
||||
from dvc.utils.pager import pager as _pager
|
||||
|
||||
_pager(text)
|
||||
else:
|
||||
cm = ui.pager() if pager else nullcontext()
|
||||
with cm:
|
||||
ui.write(text, force=force)
|
||||
|
||||
|
||||
@@ -111,14 +107,13 @@ def rich_table(
|
||||
for row, style in zip_longest(data, rs):
|
||||
table.add_row(*row, **(style or {}))
|
||||
|
||||
console = ui.rich_console
|
||||
stack = ExitStack()
|
||||
if pager:
|
||||
stack.enter_context(
|
||||
console_width(table, ui.rich_console, SHOW_MAX_WIDTH)
|
||||
)
|
||||
stack.enter_context(ui.pager())
|
||||
|
||||
if not pager:
|
||||
console.print(table)
|
||||
with stack:
|
||||
ui.write(table, styled=True)
|
||||
return
|
||||
|
||||
from dvc.utils.pager import DvcPager
|
||||
|
||||
with console_width(table, console, SHOW_MAX_WIDTH):
|
||||
with console.pager(pager=DvcPager(), styles=True):
|
||||
console.print(table)
|
||||
|
||||
+10
-7
@@ -10,7 +10,7 @@ from dvc import __version__
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.text import Text
|
||||
from dvc.ui import RichText
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -123,20 +123,23 @@ class Updater:
|
||||
current: str = None,
|
||||
color: str = "yellow",
|
||||
pkg: Optional[str] = None,
|
||||
) -> "Text":
|
||||
from rich.text import Text
|
||||
) -> "RichText":
|
||||
from dvc.ui import ui
|
||||
|
||||
current = current or self.current
|
||||
update_message = Text.from_markup(
|
||||
update_message = ui.rich_text.from_markup(
|
||||
f"You are using dvc version [bold]{current}[/]; "
|
||||
f"however, version [bold]{latest}[/] is available."
|
||||
)
|
||||
instruction = Text.from_markup(self._get_update_instructions(pkg=pkg))
|
||||
return Text.assemble(
|
||||
instruction = ui.rich_text.from_markup(
|
||||
self._get_update_instructions(pkg=pkg)
|
||||
)
|
||||
return ui.rich_text.assemble(
|
||||
"\n", update_message, "\n", instruction, style=color
|
||||
)
|
||||
|
||||
def _get_update_instructions(self, pkg: Optional[str] = None) -> str:
|
||||
@staticmethod
|
||||
def _get_update_instructions(pkg: Optional[str] = None) -> str:
|
||||
if pkg in ("osxpkg", "exe", "binary"):
|
||||
return (
|
||||
"To upgrade, uninstall dvc and reinstall from "
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Rollbacks monkeypatching of `json.encoder` by benedict.
|
||||
|
||||
It monkeypatches json to use Python-based encoder instead of C-based
|
||||
during import.
|
||||
|
||||
We rollback that monkeypatch by keeping reference to that C-based
|
||||
encoder and reinstate them after importing benedict.
|
||||
See: https://github.com/iterative/dvc/issues/6423
|
||||
https://github.com/fabiocaccamo/python-benedict/issues/62
|
||||
and the source of truth:
|
||||
https://github.com/fabiocaccamo/python-benedict/blob/c98c471065/benedict/dicts/__init__.py#L282-L285
|
||||
"""
|
||||
from json import encoder
|
||||
|
||||
try:
|
||||
c_make_encoder = encoder.c_make_encoder # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
c_make_encoder = None
|
||||
|
||||
|
||||
from benedict import benedict # noqa: E402
|
||||
|
||||
encoder.c_make_encoder = c_make_encoder # type: ignore[attr-defined]
|
||||
# Please import benedict from here lazily
|
||||
__all__ = ["benedict"]
|
||||
@@ -82,19 +82,10 @@ def chunk_dict(d: Dict[_KT, _VT], size: int = 1) -> List[Dict[_KT, _VT]]:
|
||||
|
||||
def merge_params(src: Dict, to_update: Dict) -> Dict:
|
||||
"""Recursively merges params with benedict's syntax support in-place."""
|
||||
from benedict import benedict
|
||||
from ._benedict import benedict
|
||||
|
||||
data = benedict(src)
|
||||
if src:
|
||||
data.merge(to_update, overwrite=True)
|
||||
else:
|
||||
# NOTE: the following line may seem like an unnecessary duplication
|
||||
# data.merge might affect the `src` if it's not empty, so we cannot
|
||||
# check `if src` later, as it may have been mutated already.
|
||||
data.merge(to_update, overwrite=True)
|
||||
# benedict has issues keeping references to an empty dictionary
|
||||
# see: https://github.com/iterative/dvc/issues/6374.
|
||||
src.update(data)
|
||||
data.merge(to_update, overwrite=True)
|
||||
return src
|
||||
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import io
|
||||
from contextlib import contextmanager
|
||||
|
||||
from dvc.utils.stream import IterStream
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_url(url, mode="r", encoding=None, **iter_opts):
|
||||
"""Opens an url as a readable stream.
|
||||
|
||||
Resumes on connection error.
|
||||
Url could be a string or a callable returning a string.
|
||||
"""
|
||||
assert mode in {"r", "rt", "rb"}
|
||||
|
||||
with iter_url(url, **iter_opts) as (response, it):
|
||||
bytes_stream = IterStream(it)
|
||||
|
||||
if mode == "rb":
|
||||
yield bytes_stream
|
||||
else:
|
||||
encoding = encoding or response.encoding
|
||||
yield io.TextIOWrapper(bytes_stream, encoding=encoding)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def iter_url(url, chunk_size=io.DEFAULT_BUFFER_SIZE, auth=None):
|
||||
"""Iterate over chunks requested from url."""
|
||||
import requests
|
||||
|
||||
def request(headers=None):
|
||||
the_url = url() if callable(url) else url
|
||||
response = requests.get(
|
||||
the_url, stream=True, headers=headers, auth=auth
|
||||
)
|
||||
if response.status_code == 404:
|
||||
raise FileNotFoundError(f"Can't open {the_url}")
|
||||
response.raise_for_status()
|
||||
return response
|
||||
|
||||
def gen(response):
|
||||
try:
|
||||
pos = 0
|
||||
while True:
|
||||
try:
|
||||
for chunk in response.iter_content(chunk_size):
|
||||
pos += len(chunk)
|
||||
yield chunk
|
||||
break
|
||||
except requests.ConnectionError:
|
||||
response.close()
|
||||
if response.headers.get("Accept-Ranges") != "bytes":
|
||||
raise
|
||||
|
||||
# Reopen request from where we stopped
|
||||
headers = {"Range": f"bytes={pos}-"}
|
||||
response = request(headers)
|
||||
finally:
|
||||
response.close()
|
||||
|
||||
response = request()
|
||||
it = gen(response)
|
||||
try:
|
||||
yield response, it
|
||||
finally:
|
||||
# Ensure connection is closed
|
||||
it.close()
|
||||
@@ -8,61 +8,6 @@ from dvc.istextfile import DEFAULT_CHUNK_SIZE, istextblock
|
||||
from dvc.utils import dos2unix
|
||||
|
||||
|
||||
class IterStream(io.RawIOBase):
|
||||
"""Wraps an iterator yielding bytes as a file object"""
|
||||
|
||||
def __init__(self, iterator): # pylint: disable=super-init-not-called
|
||||
self.iterator = iterator
|
||||
self.leftover = b""
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def writable(self) -> bool:
|
||||
return False
|
||||
|
||||
# Python 3 requires only .readinto() method, it still uses other ones
|
||||
# under some circumstances and falls back if those are absent. Since
|
||||
# iterator already constructs byte strings for us, .readinto() is not the
|
||||
# most optimal, so we provide .read1() too.
|
||||
|
||||
def readinto(self, b):
|
||||
try:
|
||||
n = len(b) # We're supposed to return at most this much
|
||||
chunk = self.leftover or next(self.iterator)
|
||||
output, self.leftover = chunk[:n], chunk[n:]
|
||||
|
||||
n_out = len(output)
|
||||
b[:n_out] = output
|
||||
return n_out
|
||||
except StopIteration:
|
||||
return 0 # indicate EOF
|
||||
|
||||
readinto1 = readinto
|
||||
|
||||
def read1(self, n=-1):
|
||||
try:
|
||||
chunk = self.leftover or next(self.iterator)
|
||||
except StopIteration:
|
||||
return b""
|
||||
|
||||
# Return an arbitrary number or bytes
|
||||
if n <= 0:
|
||||
self.leftover = b""
|
||||
return chunk
|
||||
|
||||
output, self.leftover = chunk[:n], chunk[n:]
|
||||
return output
|
||||
|
||||
def peek(self, n):
|
||||
while len(self.leftover) < n:
|
||||
try:
|
||||
self.leftover += next(self.iterator)
|
||||
except StopIteration:
|
||||
break
|
||||
return self.leftover[:n]
|
||||
|
||||
|
||||
class HashedStreamReader(io.IOBase):
|
||||
|
||||
PARAM_CHECKSUM = "md5"
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
"""
|
||||
This module combines schema and yaml parser into one, to provide better error
|
||||
messages through a single entrypoint `load`.
|
||||
|
||||
Used for parsing dvc.yaml, dvc.lock and .dvc files.
|
||||
|
||||
Not to be confused with strictyaml, a python library with similar motivations.
|
||||
"""
|
||||
import re
|
||||
import typing
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, TypeVar
|
||||
|
||||
from dvc.exceptions import PrettyDvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.serialize import (
|
||||
EncodingError,
|
||||
YAMLFileCorruptedError,
|
||||
parse_yaml,
|
||||
parse_yaml_for_update,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.syntax import Syntax
|
||||
from ruamel.yaml import StreamMark
|
||||
from voluptuous import MultipleInvalid
|
||||
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
from dvc.ui import RichText
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
merge_conflict_marker = re.compile("^([<=>]{7}) .*$", re.MULTILINE)
|
||||
|
||||
|
||||
def make_relpath(path: str) -> str:
|
||||
import os
|
||||
|
||||
from dvc.utils import relpath
|
||||
|
||||
rel = relpath(path)
|
||||
prefix = ""
|
||||
if not rel.startswith(".."):
|
||||
prefix = "./" if os.name == "posix" else ".\\"
|
||||
return prefix + rel
|
||||
|
||||
|
||||
def _prepare_message(message: str) -> "RichText":
|
||||
return ui.rich_text(message, style="red")
|
||||
|
||||
|
||||
def _prepare_cause(cause: str) -> "RichText":
|
||||
return ui.rich_text(cause, style="bold")
|
||||
|
||||
|
||||
def _prepare_code_snippets(
|
||||
code: str, start_line: int = 1, **kwargs: Any
|
||||
) -> "Syntax":
|
||||
from rich.syntax import Syntax
|
||||
|
||||
kwargs.setdefault("start_line", start_line)
|
||||
return Syntax(
|
||||
code,
|
||||
"yaml",
|
||||
theme="ansi_dark",
|
||||
word_wrap=True,
|
||||
line_numbers=True,
|
||||
indent_guides=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class YAMLSyntaxError(PrettyDvcException, YAMLFileCorruptedError):
|
||||
def __init__(self, path: str, yaml_text: str, exc: Exception) -> None:
|
||||
self.path: str = path
|
||||
self.yaml_text: str = yaml_text
|
||||
self.exc: Exception = exc
|
||||
|
||||
merge_conflicts = merge_conflict_marker.search(self.yaml_text)
|
||||
self.hint = " (possible merge conflicts)" if merge_conflicts else ""
|
||||
super().__init__(self.path)
|
||||
|
||||
def __pretty_exc__(self, **kwargs: Any) -> None:
|
||||
from ruamel.yaml.error import MarkedYAMLError
|
||||
|
||||
exc = self.exc.__cause__
|
||||
|
||||
if not isinstance(exc, MarkedYAMLError):
|
||||
raise ValueError("nothing to pretty-print here. :)")
|
||||
|
||||
source = self.yaml_text.splitlines()
|
||||
|
||||
def prepare_linecol(mark: "StreamMark") -> str:
|
||||
return f"in line {mark.line + 1}, column {mark.column + 1}"
|
||||
|
||||
def prepare_message(
|
||||
message: str, mark: "StreamMark" = None
|
||||
) -> "RichText":
|
||||
cause = ", ".join(
|
||||
[message.capitalize(), prepare_linecol(mark) if mark else ""]
|
||||
)
|
||||
return _prepare_cause(cause)
|
||||
|
||||
def prepare_code(mark: "StreamMark") -> "Syntax":
|
||||
line = mark.line + 1
|
||||
code = "" if line > len(source) else source[line - 1]
|
||||
return _prepare_code_snippets(code, line)
|
||||
|
||||
lines: List[object] = []
|
||||
if hasattr(exc, "context"):
|
||||
if exc.context_mark is not None:
|
||||
lines.append(
|
||||
prepare_message(str(exc.context), exc.context_mark)
|
||||
)
|
||||
if exc.context_mark is not None and (
|
||||
exc.problem is None
|
||||
or exc.problem_mark is None
|
||||
or exc.context_mark.name != exc.problem_mark.name
|
||||
or exc.context_mark.line != exc.problem_mark.line
|
||||
or exc.context_mark.column != exc.problem_mark.column
|
||||
):
|
||||
lines.extend([prepare_code(exc.context_mark), ""])
|
||||
if exc.problem is not None:
|
||||
lines.append(
|
||||
prepare_message(str(exc.problem), exc.problem_mark)
|
||||
)
|
||||
if exc.problem_mark is not None:
|
||||
lines.append(prepare_code(exc.problem_mark))
|
||||
|
||||
if lines:
|
||||
# we should not add a newline after the main message
|
||||
# if there are no other outputs
|
||||
lines.insert(0, "")
|
||||
|
||||
rel = make_relpath(self.path)
|
||||
lines.insert(0, _prepare_message(f"'{rel}' is invalid{self.hint}."))
|
||||
for line in lines:
|
||||
ui.error_write(line, styled=True)
|
||||
|
||||
|
||||
def determine_linecol(
|
||||
data, paths, max_steps=5
|
||||
) -> Tuple[Optional[int], Optional[int], int]:
|
||||
"""Determine linecol from the CommentedMap for the `paths` location.
|
||||
|
||||
CommentedMap from `ruamel.yaml` has `.lc` property from which we can read
|
||||
`.line` and `.col`. This is available in the collections type,
|
||||
i.e. list and dictionaries.
|
||||
|
||||
But this may fail on non-collection types. For example, if the `paths` is
|
||||
['stages', 'metrics'], metrics being a boolean type does not have `lc`
|
||||
prop.
|
||||
```
|
||||
stages:
|
||||
metrics: true
|
||||
```
|
||||
|
||||
To provide some context to the user, we step up to the
|
||||
path ['stages'], which being a collection type, will have `lc` prop
|
||||
with which we can find line and col.
|
||||
|
||||
This may end up being not accurate, so we try to show the same amount of
|
||||
lines of code for `n` number of steps taken upwards. In a worst case,
|
||||
it may be just 1 step (as non-collection item cannot have child items),
|
||||
but `schema validator` may provide us arbitrary path. So, this caps the
|
||||
number of steps upward to just 5. If it does not find any linecols, it'll
|
||||
abort.
|
||||
"""
|
||||
from dpath.util import get
|
||||
|
||||
step = 1
|
||||
line, col = None, None
|
||||
while paths and step < max_steps:
|
||||
value = get(data, paths, default=None)
|
||||
if value is not None:
|
||||
with suppress(AttributeError, TypeError):
|
||||
line = value.lc.line + 1
|
||||
col = value.lc.col + 1
|
||||
break
|
||||
step += 1
|
||||
*paths, _ = paths
|
||||
|
||||
return line, col, step
|
||||
|
||||
|
||||
class YAMLValidationError(PrettyDvcException):
|
||||
def __init__(
|
||||
self,
|
||||
exc: "MultipleInvalid",
|
||||
path: str = None,
|
||||
text: str = None,
|
||||
) -> None:
|
||||
self.text = text or ""
|
||||
self.exc = exc
|
||||
|
||||
rel = make_relpath(path) if path else ""
|
||||
self.path = path or ""
|
||||
|
||||
message = f"'{rel}' validation failed"
|
||||
if len(self.exc.errors) > 1:
|
||||
message += f": {len(self.exc.errors)} errors"
|
||||
super().__init__(f"{message}")
|
||||
|
||||
def _prepare_context(self, data: typing.Mapping) -> List[object]:
|
||||
lines: List[object] = []
|
||||
for index, error in enumerate(self.exc.errors):
|
||||
if index and lines[-1]:
|
||||
lines.append("")
|
||||
line, col, step = determine_linecol(data, error.path)
|
||||
parts = [error.error_message]
|
||||
if error.path:
|
||||
parts.append("in " + " -> ".join(str(p) for p in error.path))
|
||||
if line:
|
||||
parts.append(f"line {line}")
|
||||
if col:
|
||||
parts.append(f"column {col}")
|
||||
lines.append(_prepare_cause(", ".join(parts)))
|
||||
|
||||
if line:
|
||||
# we show one line above the error
|
||||
# we try to show few more lines if we could not
|
||||
# reliably figure out where the error was
|
||||
lr = (line - 1, line + step - 1)
|
||||
code = _prepare_code_snippets(self.text, line_range=lr)
|
||||
lines.append(code)
|
||||
return lines
|
||||
|
||||
def __pretty_exc__(self, **kwargs: Any) -> None:
|
||||
"""Prettify exception message."""
|
||||
from collections.abc import Mapping
|
||||
|
||||
lines: List[object] = []
|
||||
data = parse_yaml_for_update(self.text, self.path)
|
||||
if isinstance(data, Mapping):
|
||||
lines.extend(self._prepare_context(data))
|
||||
|
||||
cause = ""
|
||||
if lines:
|
||||
# we should not add a newline after the main message
|
||||
# if there are no other outputs
|
||||
lines.insert(0, "")
|
||||
else:
|
||||
# if we don't have any context to show, we'll fallback to what we
|
||||
# got from voluptuous and print them in the same line.
|
||||
cause = f": {self.exc}"
|
||||
|
||||
lines.insert(0, _prepare_message(f"{self}{cause}."))
|
||||
for line in lines:
|
||||
ui.error_write(line, styled=True)
|
||||
|
||||
|
||||
def validate(
|
||||
data: _T,
|
||||
schema: Callable[[_T], _T],
|
||||
text: str = None,
|
||||
path: str = None,
|
||||
) -> _T:
|
||||
from voluptuous import MultipleInvalid
|
||||
|
||||
try:
|
||||
return schema(data)
|
||||
except MultipleInvalid as exc:
|
||||
raise YAMLValidationError(exc, path, text) from exc
|
||||
|
||||
|
||||
def load(
|
||||
path: str,
|
||||
schema: Callable[[_T], _T] = None,
|
||||
fs: "BaseFileSystem" = None,
|
||||
encoding: str = "utf-8",
|
||||
round_trip: bool = False,
|
||||
) -> Any:
|
||||
open_fn = fs.open if fs else open
|
||||
try:
|
||||
with open_fn(path, encoding=encoding) as fd: # type: ignore
|
||||
text = fd.read()
|
||||
data = parse_yaml(text, path, typ="rt" if round_trip else "safe")
|
||||
except UnicodeDecodeError as exc:
|
||||
raise EncodingError(path, encoding) from exc
|
||||
except YAMLFileCorruptedError as exc:
|
||||
cause = exc.__cause__
|
||||
raise YAMLSyntaxError(path, text, exc) from cause
|
||||
|
||||
if schema:
|
||||
# not returning validated data, as it may remove
|
||||
# details from CommentedMap that we get from roundtrip parser
|
||||
validate(data, schema, text=text, path=path)
|
||||
return data, text
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
_BASE_VERSION = "2.7.4"
|
||||
_BASE_VERSION = "2.8.0"
|
||||
|
||||
|
||||
def _generate_version(base_version):
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
adlfs==2021.9.1
|
||||
azure-identity>=1.4.0
|
||||
knack
|
||||
@@ -1,46 +0,0 @@
|
||||
# See https://github.com/pyinstaller/pyinstaller/issues/1945
|
||||
ply>=3.9
|
||||
colorama>=0.3.9
|
||||
configobj>=5.0.6
|
||||
gitpython>3
|
||||
dulwich>=0.20.23
|
||||
pygit2>=1.5.0
|
||||
setuptools>=34.0.0
|
||||
nanotime>=0.5.2
|
||||
pyasn1>=0.4.1
|
||||
voluptuous>=0.11.7
|
||||
requests>=2.22.0
|
||||
# See https://github.com/bdcht/grandalf/issues/26
|
||||
grandalf==0.6
|
||||
distro>=1.3.0
|
||||
appdirs>=1.4.3
|
||||
ruamel.yaml>=0.17.11
|
||||
toml>=0.10.1
|
||||
funcy>=1.14
|
||||
pathspec>=0.6.0,<0.9.0
|
||||
shortuuid>=0.5.0
|
||||
tqdm>=4.45.0,<5
|
||||
packaging>=19.0
|
||||
zc.lockfile>=1.2.1
|
||||
flufl.lock>=3.2,<4
|
||||
win-unicode-console>=0.5; sys_platform == 'win32'
|
||||
pywin32>=225; sys_platform == 'win32' and python_version < '3.10'
|
||||
networkx>=2.5
|
||||
psutil>=5.8.0
|
||||
pydot>=1.2.4
|
||||
speedcopy>=2.0.1; python_version < '3.8' and sys_platform == 'win32'
|
||||
dataclasses==0.7; python_version < '3.7'
|
||||
importlib-metadata>=1.4; python_version < '3.8'
|
||||
flatten_dict>=0.4.1,<1
|
||||
tabulate>=0.8.7
|
||||
pygtrie>=2.3.2
|
||||
dpath>=2.0.2,<3
|
||||
shtab>=1.3.4,<2
|
||||
rich>=10.0.0
|
||||
dictdiffer>=0.8.1
|
||||
python-benedict>=0.21.1
|
||||
pyparsing==2.4.7
|
||||
typing_extensions>=3.10.0.2
|
||||
fsspec[http]>=2021.8.1
|
||||
aiohttp-retry==2.4.5
|
||||
diskcache>=5.2.1
|
||||
@@ -1 +0,0 @@
|
||||
pydrive2[fsspec]>=1.9.4
|
||||
@@ -1 +0,0 @@
|
||||
gcsfs==2021.8.1
|
||||
@@ -1 +0,0 @@
|
||||
pyarrow>=2.0.0; python_version < '3.10'
|
||||
@@ -1 +0,0 @@
|
||||
ossfs==2021.8.0
|
||||
@@ -1,2 +0,0 @@
|
||||
s3fs==2021.8.1
|
||||
aiobotocore[boto3]>1.0.1
|
||||
@@ -1 +0,0 @@
|
||||
sshfs>=2021.8.1
|
||||
@@ -1 +0,0 @@
|
||||
sshfs[gssapi]>=2021.8.1
|
||||
@@ -1 +0,0 @@
|
||||
tpi[ssh]>=0.0.0
|
||||
@@ -1,44 +0,0 @@
|
||||
wheel==0.37.0
|
||||
|
||||
# Test requirements
|
||||
pytest==6.2.5
|
||||
pytest-cov==2.12.1
|
||||
pytest-xdist==2.3.0
|
||||
pytest-mock==3.6.1
|
||||
pytest-lazy-fixture==0.6.3
|
||||
pytest-docker==0.10.3
|
||||
|
||||
flaky==3.7.0
|
||||
mock==4.0.3
|
||||
rangehttpserver==1.2.0
|
||||
mock-ssh-server==0.9.1
|
||||
wget==3.2
|
||||
filelock==3.0.12
|
||||
wsgidav==3.1.1
|
||||
crc32c==2.2.post0
|
||||
xmltodict==0.12.0
|
||||
google-compute-engine==2.8.13
|
||||
google-cloud-storage==1.42.1
|
||||
urllib3==1.26.6
|
||||
dvclive>=0.2.1
|
||||
|
||||
# required by collective.checkdocs
|
||||
Pygments==2.10.0
|
||||
collective.checkdocs==0.2
|
||||
pydocstyle==6.1.1
|
||||
jaraco.windows==5.6.0
|
||||
|
||||
# pylint requirements
|
||||
pylint==2.9.6
|
||||
# we use this to suppress pytest-related false positives in our tests.
|
||||
pylint-pytest==1.0.3
|
||||
# we use this to suppress some messages in tests, eg: foo/bar naming,
|
||||
# and, protected method calls in our tests
|
||||
pylint-plugin-utils==0.6
|
||||
|
||||
# type-checking
|
||||
mypy==0.910
|
||||
types-requests
|
||||
types-paramiko
|
||||
types-tabulate
|
||||
types-toml
|
||||
@@ -1 +0,0 @@
|
||||
webdav4>=0.9.1
|
||||
@@ -1 +0,0 @@
|
||||
hdfs==2.6.0
|
||||
@@ -7,7 +7,6 @@ datas = copy_metadata("adlfs", recursive=True)
|
||||
datas += copy_metadata("knack")
|
||||
datas += copy_metadata("gcsfs")
|
||||
datas += copy_metadata("pyarrow")
|
||||
datas += copy_metadata("hdfs")
|
||||
datas += copy_metadata("pydrive2")
|
||||
datas += copy_metadata("s3fs", recursive=True)
|
||||
datas += copy_metadata("boto3")
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
hiddenimports = ["win32timezone"]
|
||||
@@ -1,3 +1,171 @@
|
||||
[metadata]
|
||||
description = Git for data scientists - manage your code and data together
|
||||
name = dvc
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/rst
|
||||
license = Apache License 2.0
|
||||
license_file = LICENSE
|
||||
url = http://dvc.org
|
||||
project_urls =
|
||||
Documentation = https://dvc.org/doc
|
||||
Source = https://github.com/iterative/dvc
|
||||
download_url = https://github.com/iterative/dvc
|
||||
author = Dmitry Petrov
|
||||
author_email = [email protected]
|
||||
maintainer = Iterative
|
||||
maintainer_email = [email protected]
|
||||
keywords = data-science, data-version-control, machine-learning, git
|
||||
developer-tools, reproducibility, collaboration, ai
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
|
||||
[options]
|
||||
python_requires = >=3.6
|
||||
zip_safe = False
|
||||
packages = find:
|
||||
include_package_data = True
|
||||
install_requires =
|
||||
# See https://github.com/pyinstaller/pyinstaller/issues/1945
|
||||
ply>=3.9
|
||||
colorama>=0.3.9
|
||||
configobj>=5.0.6
|
||||
gitpython>3
|
||||
dulwich>=0.20.23
|
||||
pygit2>=1.5.0,<1.7; python_version < '3.7'
|
||||
pygit2>=1.5.0; python_version >= '3.7'
|
||||
setuptools>=34.0.0
|
||||
nanotime>=0.5.2
|
||||
pyasn1>=0.4.1
|
||||
voluptuous>=0.11.7
|
||||
requests>=2.22.0
|
||||
# See https://github.com/bdcht/grandalf/issues/26
|
||||
grandalf==0.6
|
||||
distro>=1.3.0
|
||||
appdirs>=1.4.3
|
||||
ruamel.yaml>=0.17.11
|
||||
toml>=0.10.1
|
||||
funcy>=1.14
|
||||
pathspec>=0.6.0,<0.9.0
|
||||
shortuuid>=0.5.0
|
||||
tqdm>=4.45.0,<5
|
||||
packaging>=19.0
|
||||
zc.lockfile>=1.2.1
|
||||
flufl.lock>=3.2,<4
|
||||
win-unicode-console>=0.5; sys_platform == 'win32'
|
||||
networkx>=2.5
|
||||
psutil>=5.8.0
|
||||
pydot>=1.2.4
|
||||
dataclasses>=0.7; python_version < '3.7'
|
||||
contextvars>=2.1; python_version < '3.7'
|
||||
importlib-metadata>=1.4; python_version < '3.8'
|
||||
flatten_dict>=0.4.1,<1
|
||||
tabulate>=0.8.7
|
||||
pygtrie>=2.3.2
|
||||
dpath>=2.0.2,<3
|
||||
shtab>=1.3.4,<2
|
||||
rich>=10.9.0
|
||||
dictdiffer>=0.8.1
|
||||
python-benedict>=0.24.2
|
||||
pyparsing==2.4.7
|
||||
typing_extensions>=3.7.4
|
||||
fsspec[http]>=2021.10.0
|
||||
aiohttp-retry>=2.4.5
|
||||
diskcache>=5.2.1
|
||||
|
||||
[options.extras_require]
|
||||
# gssapi should not be included in all_remotes, because it doesn't have wheels
|
||||
# for linux and mac, so it will fail to compile if user doesn't have all the
|
||||
# requirements, including kerberos itself. Once all the wheels are available,
|
||||
# we can start shipping it by default.
|
||||
all =
|
||||
%(azure)s
|
||||
%(gdrive)s
|
||||
%(gs)s
|
||||
%(hdfs)s
|
||||
%(oss)s
|
||||
%(s3)s
|
||||
%(ssh)s
|
||||
%(webdav)s
|
||||
dev =
|
||||
%(all)s
|
||||
%(tests)s
|
||||
# Extra dependencies for remote integrations
|
||||
azure =
|
||||
adlfs==2021.9.1
|
||||
azure-identity>=1.4.0
|
||||
knack
|
||||
gdrive = pydrive2[fsspec]>=1.9.4
|
||||
gs = gcsfs==2021.10.0
|
||||
hdfs =
|
||||
fsspec[arrow]; python_version < '3.10'
|
||||
oss = ossfs==2021.8.0
|
||||
s3 =
|
||||
s3fs==2021.10.0
|
||||
aiobotocore[boto3]>1.0.1
|
||||
ssh = sshfs[bcrypt]>=2021.8.1
|
||||
ssh_gssapi = sshfs[gssapi]>=2021.8.1
|
||||
webdav = webdav4>=0.9.1
|
||||
# not to break `dvc[webhdfs]`
|
||||
webhdfs =
|
||||
terraform = tpi[ssh]>=0.0.0
|
||||
tests =
|
||||
%(terraform)s
|
||||
wheel==0.37.0
|
||||
# Test requirements
|
||||
pytest==6.2.5
|
||||
pytest-cov==3.0.0
|
||||
pytest-xdist==2.4.0
|
||||
pytest-mock==3.6.1
|
||||
pytest-lazy-fixture==0.6.3
|
||||
pytest-docker==0.10.3
|
||||
flaky==3.7.0
|
||||
mock==4.0.3
|
||||
rangehttpserver==1.2.0
|
||||
mock-ssh-server==0.9.1
|
||||
wget==3.2
|
||||
filelock==3.3.0
|
||||
wsgidav==3.1.1
|
||||
crc32c==2.2.post0
|
||||
xmltodict==0.12.0
|
||||
google-compute-engine==2.8.13
|
||||
google-cloud-storage==1.42.3
|
||||
dvclive==0.4.0
|
||||
pywin32>=225; sys_platform == 'win32' and python_version < '3.10'
|
||||
hdfs==2.6.0
|
||||
# required by collective.checkdocs
|
||||
Pygments==2.10.0
|
||||
collective.checkdocs==0.2
|
||||
pydocstyle==6.1.1
|
||||
jaraco.windows==5.7.0
|
||||
# pylint requirements
|
||||
pylint==2.9.6
|
||||
# we use this to suppress pytest-related false positives in our tests.
|
||||
pylint-pytest==1.0.3
|
||||
# we use this to suppress some messages in tests, eg: foo/bar naming,
|
||||
# and, protected method calls in our tests
|
||||
pylint-plugin-utils==0.6
|
||||
# type-checking
|
||||
mypy==0.910
|
||||
types-requests==2.25.9
|
||||
types-paramiko==2.7.1
|
||||
types-tabulate==0.8.2
|
||||
types-toml==0.10.0
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
tests
|
||||
tests.*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
dvc = dvc.main:main
|
||||
|
||||
[flake8]
|
||||
ignore=
|
||||
E203, # Whitespace before ':'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools import setup
|
||||
from setuptools.command.build_py import build_py as _build_py
|
||||
|
||||
# Prevents pkg_resources import in entry point script,
|
||||
@@ -16,12 +17,23 @@ except ImportError:
|
||||
|
||||
# Read package meta-data from version.py
|
||||
# see https://packaging.python.org/guides/single-sourcing-package-version/
|
||||
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
version_path = os.path.join(pkg_dir, "dvc", "version.py")
|
||||
spec = importlib.util.spec_from_file_location("dvc.version", version_path)
|
||||
dvc_version = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(dvc_version)
|
||||
version = dvc_version.__version__ # noqa: F821
|
||||
try:
|
||||
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
version_path = os.path.join(pkg_dir, "dvc", "version.py")
|
||||
spec = importlib.util.spec_from_file_location("dvc.version", version_path)
|
||||
dvc_version = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(dvc_version)
|
||||
version = dvc_version.__version__ # noqa: F821
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# Dependabot seem to stop working when we don't handle this except block.
|
||||
# Most likely, it's because of the restrictions in execution of the module.
|
||||
# We workaround it, and print the error message if it also happens on other
|
||||
# installations (though this message may likely be suppressed by build
|
||||
# tools, eg: pip only shows this message on `--verbose` mode).
|
||||
import sys
|
||||
|
||||
print("Could not load version info: ", exc, file=sys.stderr)
|
||||
version = "UNKNOWN"
|
||||
|
||||
|
||||
# To achieve consistency between the build version and the one provided
|
||||
@@ -35,65 +47,20 @@ version = dvc_version.__version__ # noqa: F821
|
||||
# - https://github.com/python/mypy
|
||||
class build_py(_build_py):
|
||||
def pin_version(self):
|
||||
path = os.path.join(self.build_lib, "dvc")
|
||||
self.mkpath(path)
|
||||
with open(os.path.join(path, "version.py"), "w") as fobj:
|
||||
fobj.write("# AUTOGENERATED at build time by setup.py\n")
|
||||
fobj.write(f'__version__ = "{version}"\n')
|
||||
ver_file = Path(self.build_lib) / "dvc" / "version.py"
|
||||
self.mkpath(os.fspath(ver_file.parent))
|
||||
ver_file.write_text(
|
||||
dedent(
|
||||
f"""\
|
||||
# AUTOGENERATED at build time by setup.py
|
||||
__version__ = "{version}"
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.execute(self.pin_version, ())
|
||||
_build_py.run(self)
|
||||
|
||||
|
||||
# Extra dependencies for remote integrations
|
||||
requirements = {
|
||||
path.stem: path.read_text().strip().splitlines()
|
||||
for path in Path("requirements").glob("*.txt")
|
||||
}
|
||||
|
||||
# gssapi should not be included in all_remotes, because it doesn't have wheels
|
||||
# for linux and mac, so it will fail to compile if user doesn't have all the
|
||||
# requirements, including kerberos itself. Once all the wheels are available,
|
||||
# we can start shipping it by default.
|
||||
|
||||
install_requires = requirements.pop("default")
|
||||
requirements["all"] = [
|
||||
requirements
|
||||
for key, requirements in requirements.items()
|
||||
if key not in ("tests", "ssh_gssapi", "terraform")
|
||||
]
|
||||
requirements["tests"] += requirements["terraform"]
|
||||
requirements["dev"] = requirements["all"] + requirements["tests"]
|
||||
|
||||
setup(
|
||||
name="dvc",
|
||||
version=version,
|
||||
description="Git for data scientists - manage your code and data together",
|
||||
long_description=open("README.rst", encoding="UTF-8").read(),
|
||||
author="Dmitry Petrov",
|
||||
author_email="[email protected]",
|
||||
maintainer="Iterative",
|
||||
maintainer_email="[email protected]",
|
||||
download_url="https://github.com/iterative/dvc",
|
||||
license="Apache License 2.0",
|
||||
install_requires=install_requires,
|
||||
extras_require=requirements,
|
||||
keywords="data-science data-version-control machine-learning git"
|
||||
" developer-tools reproducibility collaboration ai",
|
||||
python_requires=">=3.6",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
include_package_data=True,
|
||||
url="http://dvc.org",
|
||||
entry_points={"console_scripts": ["dvc = dvc.main:main"]},
|
||||
cmdclass={"build_py": build_py},
|
||||
zip_safe=False,
|
||||
)
|
||||
setup(version=version, cmdclass={"build_py": build_py})
|
||||
|
||||
+3
-2
@@ -19,11 +19,12 @@ REMOTES = {
|
||||
"azure": False,
|
||||
"gdrive": False,
|
||||
"gs": False,
|
||||
"hdfs": False,
|
||||
"hdfs": True,
|
||||
"real_hdfs": False,
|
||||
"http": True,
|
||||
"oss": False,
|
||||
"s3": False,
|
||||
"ssh": True,
|
||||
"ssh": False,
|
||||
"webdav": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
version: '3.2'
|
||||
services:
|
||||
azurite:
|
||||
image: mcr.microsoft.com/azure-storage/azurite:3.13.1
|
||||
image: mcr.microsoft.com/azure-storage/azurite:3.14.2
|
||||
command: azurite -L -l /data --blobHost 0.0.0.0 --queueHost 0.0.0.0
|
||||
ports:
|
||||
- "10000"
|
||||
|
||||
motoserver:
|
||||
image: motoserver/moto
|
||||
ports:
|
||||
- "5000"
|
||||
|
||||
oss:
|
||||
image: rkuprieiev/oss-emulator
|
||||
ports:
|
||||
@@ -25,3 +27,13 @@ services:
|
||||
- "50010:50010"
|
||||
- "50075:50075"
|
||||
- "50070"
|
||||
|
||||
openssh-server:
|
||||
image: ghcr.io/linuxserver/openssh-server
|
||||
environment:
|
||||
- USER_NAME=user
|
||||
- PUBLIC_KEY_FILE=/tmp/key
|
||||
ports:
|
||||
- 2222
|
||||
volumes:
|
||||
- ./remotes/user.key.pub:/tmp/key
|
||||
|
||||
@@ -24,7 +24,6 @@ def test_init(tmp_dir, dvc):
|
||||
"default": {
|
||||
"cmd": script,
|
||||
"deps": ["data", "src"],
|
||||
"live": {"dvclive": {"html": True, "summary": True}},
|
||||
"metrics": [{"metrics.json": {"cache": False}}],
|
||||
"outs": ["models"],
|
||||
"params": ["foo"],
|
||||
|
||||
@@ -513,7 +513,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
|
||||
ref_info2 = first(exp_refs_by_rev(scm, rev2))
|
||||
|
||||
capsys.readouterr()
|
||||
assert main(["exp", "show", "--show-csv"]) == 0
|
||||
assert main(["exp", "show", "--csv"]) == 0
|
||||
cap = capsys.readouterr()
|
||||
assert (
|
||||
"Experiment,rev,typ,Created,parent,metrics.yaml:foo,params.yaml:foo"
|
||||
|
||||
+10
-7
@@ -496,27 +496,27 @@ def test_should_update_state_entry_for_directory_after_add(
|
||||
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 3
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 3
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
|
||||
ls = "dir" if os.name == "nt" else "ls"
|
||||
ret = main(
|
||||
["run", "--single-stage", "-d", "data", "{} {}".format(ls, "data")]
|
||||
)
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 3
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
|
||||
os.rename("data", "data" + ".back")
|
||||
ret = main(["checkout"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 3
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert file_md5_counter.mock.call_count == 3
|
||||
assert file_md5_counter.mock.call_count == 4
|
||||
|
||||
|
||||
class TestAddCommit(TestDvc):
|
||||
@@ -537,13 +537,15 @@ def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
|
||||
counter = mocker.spy(dvc_module.objects.stage, "_get_tree_obj")
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 2
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 2
|
||||
|
||||
ret = main(["status"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 1
|
||||
assert counter.mock.call_count == 2
|
||||
|
||||
|
||||
class TestShouldPlaceStageInDataDirIfRepositoryBelowSymlink(TestDvc):
|
||||
@@ -951,7 +953,8 @@ def test_add_with_cache_link_error(tmp_dir, dvc, mocker, capsys):
|
||||
tmp_dir.gen("foo", "foo")
|
||||
|
||||
mocker.patch(
|
||||
"dvc.checkout._do_link", side_effect=DvcException("link failed")
|
||||
"dvc.objects.checkout._do_link",
|
||||
side_effect=DvcException("link failed"),
|
||||
)
|
||||
dvc.add("foo")
|
||||
err = capsys.readouterr()[1]
|
||||
|
||||
@@ -10,11 +10,9 @@ from dvc.dvcfile import (
|
||||
ParametrizedDumpError,
|
||||
SingleStageFile,
|
||||
)
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileDoesNotExistError,
|
||||
StageFileFormatError,
|
||||
)
|
||||
from dvc.stage.exceptions import StageFileDoesNotExistError
|
||||
from dvc.stage.loader import StageNotFound
|
||||
from dvc.utils.strictyaml import YAMLValidationError
|
||||
|
||||
|
||||
def test_run_load_one_for_multistage(tmp_dir, dvc):
|
||||
@@ -245,14 +243,14 @@ def test_remove_stage_on_lockfile_format_error(tmp_dir, dvc, run_copy):
|
||||
lock_data["gibberish"] = True
|
||||
data["gibberish"] = True
|
||||
(tmp_dir / lock_file.relpath).dump(lock_data)
|
||||
with pytest.raises(StageFileFormatError):
|
||||
with pytest.raises(YAMLValidationError):
|
||||
dvc_file.remove_stage(stage)
|
||||
|
||||
lock_file.remove()
|
||||
dvc_file.dump(stage, update_pipeline=False)
|
||||
|
||||
(tmp_dir / dvc_file.relpath).dump(data)
|
||||
with pytest.raises(StageFileFormatError):
|
||||
with pytest.raises(YAMLValidationError):
|
||||
dvc_file.remove_stage(stage)
|
||||
|
||||
|
||||
|
||||
+34
-12
@@ -10,19 +10,20 @@ from dvc.render.utils import get_files
|
||||
|
||||
LIVE_SCRIPT = dedent(
|
||||
"""
|
||||
import dvclive
|
||||
from dvclive import Live
|
||||
import sys
|
||||
r = 2
|
||||
metrics_logger = Live()
|
||||
for i in range(r):
|
||||
dvclive.log("loss", 1-i/r)
|
||||
dvclive.log("accuracy", i/r)
|
||||
dvclive.next_step()"""
|
||||
metrics_logger.log("loss", 1-i/r)
|
||||
metrics_logger.log("accuracy", i/r)
|
||||
metrics_logger.next_step()"""
|
||||
)
|
||||
|
||||
LIVE_CHECKPOINT_SCRIPT = dedent(
|
||||
"""
|
||||
import os
|
||||
import dvclive
|
||||
from dvclive import Live
|
||||
|
||||
def read(path):
|
||||
value=0
|
||||
@@ -40,15 +41,16 @@ LIVE_CHECKPOINT_SCRIPT = dedent(
|
||||
|
||||
r = 3
|
||||
checkpoint_file = "checkpoint"
|
||||
metrics_logger = Live()
|
||||
|
||||
value = read(checkpoint_file)
|
||||
for i in range(1,r):
|
||||
m = i + value
|
||||
dump(m, checkpoint_file)
|
||||
|
||||
dvclive.log("metric1", m)
|
||||
dvclive.log("metric2", m * 2)
|
||||
dvclive.next_step()"""
|
||||
metrics_logger.log("metric1", m)
|
||||
metrics_logger.log("metric2", m * 2)
|
||||
metrics_logger.next_step()"""
|
||||
)
|
||||
|
||||
|
||||
@@ -242,12 +244,13 @@ def test_dvc_generates_html_during_run(tmp_dir, dvc, mocker, live_stage):
|
||||
|
||||
script = dedent(
|
||||
"""
|
||||
import dvclive
|
||||
from dvclive import Live
|
||||
import sys
|
||||
import time
|
||||
dvclive.log("loss", 1/2)
|
||||
dvclive.log("accuracy", 1/2)
|
||||
dvclive.next_step()
|
||||
metrics_logger = Live()
|
||||
metrics_logger.log("loss", 1/2)
|
||||
metrics_logger.log("accuracy", 1/2)
|
||||
metrics_logger.next_step()
|
||||
time.sleep({})""".format(
|
||||
str(monitor_await_time * 10)
|
||||
)
|
||||
@@ -255,3 +258,22 @@ def test_dvc_generates_html_during_run(tmp_dir, dvc, mocker, live_stage):
|
||||
live_stage(summary=True, live="logs", code=script)
|
||||
|
||||
assert show_spy.call_count == 2
|
||||
|
||||
|
||||
def test_dvclive_stage_with_different_wdir(tmp_dir, scm, dvc):
|
||||
(tmp_dir / "subdir").gen("train.py", LIVE_SCRIPT)
|
||||
(tmp_dir / "subdir" / "params.yaml").dump({"foo": 1})
|
||||
dvc.stage.add(
|
||||
cmd="python train.py",
|
||||
params=["foo"],
|
||||
deps=["train.py"],
|
||||
name="live_stage",
|
||||
live="dvclive",
|
||||
live_no_html=True,
|
||||
wdir="subdir",
|
||||
)
|
||||
|
||||
dvc.reproduce()
|
||||
|
||||
assert (tmp_dir / "subdir" / "dvclive").is_dir()
|
||||
assert (tmp_dir / "subdir" / "dvclive.json").is_file()
|
||||
|
||||
@@ -5,11 +5,12 @@ from operator import itemgetter
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.dvcfile import PIPELINE_LOCK, Lockfile, LockfileCorruptedError
|
||||
from dvc.dvcfile import PIPELINE_LOCK, Lockfile
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.stage.utils import split_params_deps
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.serialize import dumps_yaml, parse_yaml_for_update
|
||||
from dvc.utils.strictyaml import YAMLValidationError, make_relpath
|
||||
from tests.func.test_run_multistage import supported_params
|
||||
|
||||
FS_STRUCTURE = {
|
||||
@@ -207,13 +208,14 @@ def test_migrates_v1_lockfile_to_v2_during_dump(
|
||||
def test_lockfile_invalid_versions(tmp_dir, dvc, version_info):
|
||||
lockdata = {**version_info, "stages": {"foo": {"cmd": "echo foo"}}}
|
||||
(tmp_dir / "dvc.lock").dump(lockdata)
|
||||
with pytest.raises(LockfileCorruptedError) as exc_info:
|
||||
with pytest.raises(YAMLValidationError) as exc_info:
|
||||
Lockfile(dvc, tmp_dir / "dvc.lock").load()
|
||||
|
||||
assert str(exc_info.value) == "Lockfile 'dvc.lock' is corrupted."
|
||||
rel = make_relpath("dvc.lock")
|
||||
assert f"'{rel}' validation failed" in str(exc_info.value)
|
||||
assert (
|
||||
str(exc_info.value.__cause__) == "'dvc.lock' format error: "
|
||||
f"invalid schema version {version_info['schema']}, "
|
||||
str(exc_info.value.__cause__)
|
||||
== f"invalid schema version {version_info['schema']}, "
|
||||
"expected one of ['2.0'] for dictionary value @ "
|
||||
"data['schema']"
|
||||
)
|
||||
|
||||
@@ -9,14 +9,14 @@ from dvc.main import main
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo, lock_repo
|
||||
from dvc.stage import PipelineStage, Stage
|
||||
from dvc.stage.exceptions import StageFileFormatError
|
||||
from dvc.stage.run import run_stage
|
||||
from dvc.utils.serialize import dump_yaml, load_yaml
|
||||
from dvc.utils.strictyaml import YAMLValidationError
|
||||
from tests.basic_env import TestDvc
|
||||
|
||||
|
||||
def test_cmd_obj():
|
||||
with pytest.raises(StageFileFormatError):
|
||||
with pytest.raises(YAMLValidationError):
|
||||
SingleStageFile.validate({Stage.PARAM_CMD: {}})
|
||||
|
||||
|
||||
@@ -33,10 +33,10 @@ def test_cmd_str():
|
||||
|
||||
|
||||
def test_object():
|
||||
with pytest.raises(StageFileFormatError):
|
||||
with pytest.raises(YAMLValidationError):
|
||||
SingleStageFile.validate({Stage.PARAM_DEPS: {}})
|
||||
|
||||
with pytest.raises(StageFileFormatError):
|
||||
with pytest.raises(YAMLValidationError):
|
||||
SingleStageFile.validate({Stage.PARAM_OUTS: {}})
|
||||
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ from dvc.path_info import PathInfo
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileDoesNotExistError,
|
||||
StageFileFormatError,
|
||||
StageNameUnspecified,
|
||||
StageNotFound,
|
||||
)
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.strictyaml import YAMLValidationError
|
||||
|
||||
|
||||
def test_collect(tmp_dir, scm, dvc, run_copy):
|
||||
@@ -450,7 +450,7 @@ def test_collect_repo_callback(tmp_dir, dvc, mocker):
|
||||
|
||||
file_path, exc = mock.call_args[0]
|
||||
assert file_path == PIPELINE_FILE
|
||||
assert isinstance(exc, StageFileFormatError)
|
||||
assert isinstance(exc, YAMLValidationError)
|
||||
|
||||
|
||||
def test_gitignored_collect_repo(tmp_dir, dvc, scm):
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.repo import Repo
|
||||
from dvc.state import State
|
||||
from dvc.utils import file_md5
|
||||
|
||||
@@ -77,3 +79,17 @@ def test_get_unused_links(tmp_dir, dvc):
|
||||
)
|
||||
== {"bar"}
|
||||
)
|
||||
|
||||
|
||||
def test_state_dir_config(make_tmp_dir, dvc):
|
||||
assert dvc.state.tmp_dir == dvc.tmp_dir
|
||||
|
||||
index_dir = str(make_tmp_dir("tmp_index"))
|
||||
repo = Repo(config={"state": {"dir": index_dir}})
|
||||
assert os.path.dirname(repo.state.tmp_dir) == os.path.join(
|
||||
index_dir, ".dvc"
|
||||
)
|
||||
assert re.match(
|
||||
r"^test_state_dir_config0-([0-9a-f]+)$",
|
||||
os.path.basename(repo.state.tmp_dir),
|
||||
)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.system import System
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name != "nt", reason="Windows specific")
|
||||
def test_getdirinfo(tmp_path):
|
||||
# simulate someone holding an exclusive access
|
||||
locked = str(tmp_path / "locked")
|
||||
fd = os.open(locked, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
|
||||
try:
|
||||
# should not raise WinError
|
||||
System._getdirinfo(locked)
|
||||
finally:
|
||||
os.close(fd)
|
||||
@@ -0,0 +1,396 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from ruamel.yaml import __with_libyaml__ as ruamel_clib
|
||||
|
||||
from dvc.main import main
|
||||
from dvc.ui import ui
|
||||
|
||||
DUPLICATE_KEYS = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: python train.py
|
||||
cmd: python train.py
|
||||
"""
|
||||
|
||||
DUPLICATE_KEYS_OUTPUT = """\
|
||||
'./dvc.yaml' is invalid.
|
||||
|
||||
While constructing a mapping, in line 3, column 5
|
||||
3 │ cmd: python train.py
|
||||
|
||||
Found duplicate key "cmd" with value "python train.py" (original value:\
|
||||
"python \ntrain.py"), in line 4, column 5
|
||||
4 │ cmd: python train.py"""
|
||||
|
||||
|
||||
MAPPING_VALUES_NOT_ALLOWED = """\
|
||||
stages:
|
||||
stage1
|
||||
cmd: python script.py
|
||||
"""
|
||||
|
||||
MAPPING_VALUES_NOT_ALLOWED_OUTPUT = """\
|
||||
'./dvc.yaml' is invalid.
|
||||
|
||||
Mapping values are not allowed {}, in line 3, column 8
|
||||
3 │ cmd: python script.py""".format(
|
||||
"in this context" if ruamel_clib else "here"
|
||||
)
|
||||
|
||||
|
||||
NO_HYPHEN_INDICATOR_IN_BLOCK = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: python script.py
|
||||
outs:
|
||||
- logs:
|
||||
cache: false
|
||||
metrics:
|
||||
"""
|
||||
|
||||
NO_HYPHEN_INDICATOR_IN_BLOCK_OUTPUT = """\
|
||||
'./dvc.yaml' is invalid.
|
||||
|
||||
While parsing a block collection, in line 5, column 7
|
||||
5 │ - logs:
|
||||
|
||||
{}, in line 7, column 7
|
||||
7 │ metrics:""".format(
|
||||
"Did not find expected '-' indicator"
|
||||
if ruamel_clib
|
||||
else "Expected <block end>, but found '?'"
|
||||
)
|
||||
|
||||
|
||||
UNCLOSED_SCALAR = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: python script.py
|
||||
desc: "this is my stage one
|
||||
"""
|
||||
|
||||
UNCLOSED_SCALAR_OUTPUT = """\
|
||||
'./dvc.yaml' is invalid.
|
||||
|
||||
While scanning a quoted scalar, in line 4, column 11
|
||||
4 │ desc: "this is my stage one
|
||||
|
||||
Found unexpected end of stream, in line 5, column 1
|
||||
5"""
|
||||
|
||||
|
||||
NOT_A_DICT = "3"
|
||||
NOT_A_DICT_OUTPUT = "'./dvc.yaml' validation failed: expected a dictionary.\n"
|
||||
|
||||
|
||||
EMPTY_STAGE = """\
|
||||
stages:
|
||||
stage1:
|
||||
"""
|
||||
|
||||
EMPTY_STAGE_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected a dictionary, in stages -> stage1, line 2, column 3
|
||||
1 stages:
|
||||
2 stage1:
|
||||
3"""
|
||||
|
||||
|
||||
MISSING_CMD = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: {}
|
||||
"""
|
||||
|
||||
MISSING_CMD_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected str, in stages -> stage1 -> cmd, line 3, column 10
|
||||
2 stage1:
|
||||
3 │ cmd: {}"""
|
||||
|
||||
|
||||
DEPS_AS_DICT = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: python script.py
|
||||
deps:
|
||||
- src:
|
||||
"""
|
||||
|
||||
DEPS_AS_DICT_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected str, in stages -> stage1 -> deps -> 0, line 5, column 9
|
||||
4 │ deps:
|
||||
5 │ - src:
|
||||
"""
|
||||
|
||||
OUTS_AS_STR = """\
|
||||
stages:
|
||||
train:
|
||||
cmd:
|
||||
- python train.py
|
||||
deps:
|
||||
- config.cfg
|
||||
outs:
|
||||
models/"""
|
||||
|
||||
OUTS_AS_STR_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected a list, in stages -> train -> outs, line 3, column 5
|
||||
2 train:
|
||||
3 │ cmd:
|
||||
4 │ - python train.py"""
|
||||
|
||||
|
||||
NULL_VALUE_ON_OUTS = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: python script.py
|
||||
outs:
|
||||
- logs:
|
||||
cache: false
|
||||
persist: true
|
||||
remote:
|
||||
"""
|
||||
|
||||
NULL_VALUE_ON_OUTS_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected str, in stages -> stage1 -> outs -> 0 -> logs -> remote, line 6,\
|
||||
column\n9
|
||||
5 │ - logs:
|
||||
6 │ │ cache: false
|
||||
7 │ │ persist: true"""
|
||||
|
||||
ADDITIONAL_KEY_ON_OUTS = """\
|
||||
stages:
|
||||
stage1:
|
||||
cmd: python script.py
|
||||
outs:
|
||||
- logs:
|
||||
cache: false
|
||||
not_existing_key: false
|
||||
"""
|
||||
|
||||
ADDITIONAL_KEY_ON_OUTS_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
extra keys not allowed, in stages -> stage1 -> outs -> 0 -> logs -> \n\
|
||||
not_existing_key, line 6, column 9
|
||||
5 │ - logs:
|
||||
6 │ │ cache: false
|
||||
7 │ │ not_existing_key: false"""
|
||||
|
||||
|
||||
FOREACH_SCALAR_VALUE = """\
|
||||
stages:
|
||||
group:
|
||||
foreach: 3
|
||||
do:
|
||||
cmd: python script${i}.py
|
||||
"""
|
||||
|
||||
FOREACH_SCALAR_VALUE_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected dict, in stages -> group -> foreach, line 3, column 5
|
||||
2 group:
|
||||
3 │ foreach: 3
|
||||
4 │ do:"""
|
||||
|
||||
FOREACH_DO_NULL = """\
|
||||
stages:
|
||||
stage1:
|
||||
foreach: [1,2,3]
|
||||
do:
|
||||
"""
|
||||
|
||||
|
||||
FOREACH_DO_NULL_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
expected a dictionary, in stages -> stage1 -> do, line 3, column 5
|
||||
2 stage1:
|
||||
3 │ foreach: [1,2,3]
|
||||
4 │ do:"""
|
||||
|
||||
|
||||
FOREACH_DO_MISSING_CMD = """\
|
||||
stages:
|
||||
stage1:
|
||||
foreach: [1,2,3]
|
||||
do:
|
||||
outs:
|
||||
- ${item}
|
||||
"""
|
||||
|
||||
|
||||
FOREACH_WITH_CMD_DO_MISSING = """\
|
||||
stages:
|
||||
stage1:
|
||||
foreach: [1,2,3]
|
||||
cmd: python script${item}.py
|
||||
"""
|
||||
|
||||
|
||||
FOREACH_WITH_CMD_DO_MISSING_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed: 2 errors.
|
||||
|
||||
extra keys not allowed, in stages -> stage1 -> cmd, line 3, column 5
|
||||
2 stage1:
|
||||
3 │ foreach: [1,2,3]
|
||||
4 │ cmd: python script${item}.py
|
||||
|
||||
required key not provided, in stages -> stage1 -> do, line 3, column 5
|
||||
2 stage1:
|
||||
3 │ foreach: [1,2,3]
|
||||
4 │ cmd: python script${item}.py"""
|
||||
|
||||
|
||||
FOREACH_DO_MISSING_CMD_OUTPUT = """\
|
||||
'./dvc.yaml' validation failed.
|
||||
|
||||
required key not provided, in stages -> stage1 -> do -> cmd, line 5, column 7
|
||||
4 │ do:
|
||||
5 │ outs:
|
||||
6 │ - ${item}"""
|
||||
|
||||
|
||||
MERGE_CONFLICTS = """\
|
||||
stages:
|
||||
load_data:
|
||||
<<<<<<< HEAD
|
||||
cmd: python src/load_data.py
|
||||
deps:
|
||||
- src/load_data.py
|
||||
=======
|
||||
cmd: python load_data.py
|
||||
deps:
|
||||
- load_data.py
|
||||
>>>>>>> branch
|
||||
outs:
|
||||
- data
|
||||
"""
|
||||
|
||||
MERGE_CONFLICTS_OUTPUT = """\
|
||||
'./dvc.yaml' is invalid (possible merge conflicts).
|
||||
|
||||
While scanning a simple key, in line 3, column 1
|
||||
3 <<<<<<< HEAD
|
||||
|
||||
Could not find expected ':', in line 4, column 8
|
||||
4 │ cmd: python src/load_data.py"""
|
||||
|
||||
|
||||
examples = {
|
||||
# on parse errors
|
||||
"duplicate_keys": (DUPLICATE_KEYS, DUPLICATE_KEYS_OUTPUT),
|
||||
"mapping_values_not_allowed": (
|
||||
MAPPING_VALUES_NOT_ALLOWED,
|
||||
MAPPING_VALUES_NOT_ALLOWED_OUTPUT,
|
||||
),
|
||||
"no_hyphen_block": (
|
||||
NO_HYPHEN_INDICATOR_IN_BLOCK,
|
||||
NO_HYPHEN_INDICATOR_IN_BLOCK_OUTPUT,
|
||||
),
|
||||
"unclosed_scalar": (UNCLOSED_SCALAR, UNCLOSED_SCALAR_OUTPUT),
|
||||
# schema validation errors
|
||||
"not_a_dict": (NOT_A_DICT, NOT_A_DICT_OUTPUT),
|
||||
"empty_stage": (EMPTY_STAGE, EMPTY_STAGE_OUTPUT),
|
||||
"missing_cmd": (MISSING_CMD, MISSING_CMD_OUTPUT),
|
||||
"deps_as_dict": (DEPS_AS_DICT, DEPS_AS_DICT_OUTPUT),
|
||||
"outs_as_str": (OUTS_AS_STR, OUTS_AS_STR_OUTPUT),
|
||||
"null_value_on_outs": (NULL_VALUE_ON_OUTS, NULL_VALUE_ON_OUTS_OUTPUT),
|
||||
"additional_key_on_outs": (
|
||||
ADDITIONAL_KEY_ON_OUTS,
|
||||
ADDITIONAL_KEY_ON_OUTS_OUTPUT,
|
||||
),
|
||||
"foreach_scalar": (FOREACH_SCALAR_VALUE, FOREACH_SCALAR_VALUE_OUTPUT),
|
||||
"foreach_do_do_null": (FOREACH_DO_NULL, FOREACH_DO_NULL_OUTPUT),
|
||||
"foreach_do_missing_cmd": (
|
||||
FOREACH_DO_MISSING_CMD,
|
||||
FOREACH_DO_MISSING_CMD_OUTPUT,
|
||||
),
|
||||
"foreach_unknown_cmd_missing_do": (
|
||||
FOREACH_WITH_CMD_DO_MISSING,
|
||||
FOREACH_WITH_CMD_DO_MISSING_OUTPUT,
|
||||
),
|
||||
# merge conflicts
|
||||
"merge_conflicts": (MERGE_CONFLICTS, MERGE_CONFLICTS_OUTPUT),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text, expected", examples.values(), ids=examples.keys()
|
||||
)
|
||||
def test_exceptions(tmp_dir, dvc, capsys, text, expected, mocker):
|
||||
# make it always return posix path, easier for validating error messages
|
||||
mocker.patch(
|
||||
"dvc.utils.strictyaml.make_relpath", return_value="./dvc.yaml"
|
||||
)
|
||||
|
||||
console = ui.error_console
|
||||
original_printer = console.print
|
||||
|
||||
def print_with_fixed_width(*args, **kwargs):
|
||||
console.options.min_width = console.options.max_width = 80
|
||||
console.width = kwargs["width"] = 80
|
||||
return original_printer(*args, **kwargs)
|
||||
|
||||
mocker.patch.object(console, "print", print_with_fixed_width)
|
||||
|
||||
tmp_dir.gen("dvc.yaml", text)
|
||||
|
||||
capsys.readouterr() # clear outputs
|
||||
assert main(["stage", "list"]) != 0
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
assert not out
|
||||
assert expected in err
|
||||
|
||||
|
||||
def test_make_relpath(tmp_dir, monkeypatch):
|
||||
from dvc.utils.strictyaml import make_relpath
|
||||
|
||||
path = tmp_dir / "dvc.yaml"
|
||||
expected_path = "./dvc.yaml" if os.name == "posix" else ".\\dvc.yaml"
|
||||
assert make_relpath(path) == expected_path
|
||||
|
||||
(tmp_dir / "dir").mkdir(exist_ok=True)
|
||||
monkeypatch.chdir("dir")
|
||||
|
||||
expected_path = "../dvc.yaml" if os.name == "posix" else "..\\dvc.yaml"
|
||||
assert make_relpath(path) == expected_path
|
||||
|
||||
|
||||
def test_fallback_exception_message(tmp_dir, dvc, mocker, caplog):
|
||||
# When trying to pretty print exception messages, we fallback to old way
|
||||
# of printing things.
|
||||
mocker.patch(
|
||||
"dvc.utils.strictyaml.YAMLSyntaxError.__pretty_exc__",
|
||||
side_effect=ValueError,
|
||||
)
|
||||
mocker.patch(
|
||||
"dvc.utils.strictyaml.YAMLValidationError.__pretty_exc__",
|
||||
side_effect=ValueError,
|
||||
)
|
||||
|
||||
# syntax errors
|
||||
dvc_file = tmp_dir / "dvc.yaml"
|
||||
dvc_file.write_text(MAPPING_VALUES_NOT_ALLOWED)
|
||||
assert main(["stage", "list"]) != 0
|
||||
assert (
|
||||
"unable to read: 'dvc.yaml', "
|
||||
"YAML file structure is corrupted" in caplog.text
|
||||
)
|
||||
|
||||
caplog.clear()
|
||||
# validation error
|
||||
dvc_file.dump({"stages": {"stage1": None}})
|
||||
assert main(["stage", "list"]) != 0
|
||||
assert "dvc.yaml' validation failed" in caplog.text
|
||||
@@ -32,14 +32,9 @@ from .s3 import ( # noqa: F401
|
||||
s3_fake_creds_file,
|
||||
s3_server,
|
||||
)
|
||||
from .ssh import ( # noqa: F401; noqa: F401
|
||||
SSHMocked,
|
||||
ssh,
|
||||
ssh_connection,
|
||||
ssh_server,
|
||||
)
|
||||
from .ssh import SSH, ssh, ssh_connection, ssh_server # noqa: F401; noqa: F401
|
||||
from .webdav import Webdav, webdav, webdav_server # noqa: F401
|
||||
from .webhdfs import WebHDFS, real_webhdfs, webhdfs # noqa: F401
|
||||
from .webhdfs import WebHDFS, webhdfs # noqa: F401
|
||||
|
||||
TEST_REMOTE = "upstream"
|
||||
TEST_CONFIG = {
|
||||
@@ -94,7 +89,7 @@ def docker_services(
|
||||
# as it might result in network conflicts. Inspired by:
|
||||
# https://github.com/pytest-dev/pytest-xdist#making-session-scoped-fixtures-execute-only-once
|
||||
lockfile = tmp_path_factory.getbasetemp().parent / "docker-compose.lock"
|
||||
with FileLock(str(lockfile)):
|
||||
with FileLock(str(lockfile)): # pylint:disable=abstract-class-instantiated
|
||||
executor.execute("up --build -d")
|
||||
|
||||
return Services(executor)
|
||||
|
||||
+43
-13
@@ -73,7 +73,7 @@ class HDFS(Base, URLInfo): # pylint: disable=abstract-method
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def hadoop(test_config):
|
||||
test_config.requires("hdfs")
|
||||
test_config.requires("real_hdfs")
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
pytest.skip("pyarrow is not available for 3.10 yet.")
|
||||
@@ -143,9 +143,17 @@ def hdfs_server(hadoop, docker_compose, docker_services):
|
||||
fobj.write(b"test")
|
||||
return True
|
||||
except (pyarrow.ArrowException, OSError):
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
docker_services.wait_until_responsive(timeout=30.0, pause=5, check=_check)
|
||||
try:
|
||||
docker_services.wait_until_responsive(
|
||||
timeout=30.0, pause=5, check=_check
|
||||
)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
pytest.skip("couldn't start hdfs server")
|
||||
|
||||
return {"hdfs": port, "webhdfs": web_port}
|
||||
|
||||
@@ -195,8 +203,7 @@ def md5md5crc32c(path):
|
||||
return "000002000000000000000000" + md5md5.hexdigest()
|
||||
|
||||
|
||||
def hadoop_fs_checksum(path_info):
|
||||
|
||||
def hadoop_fs_checksum(_, path_info):
|
||||
return md5md5crc32c(Path(_hdfs_root.name) / path_info.path.lstrip("/"))
|
||||
|
||||
|
||||
@@ -216,25 +223,48 @@ class FakeHadoopFileSystem:
|
||||
path.allow_not_found,
|
||||
path.recursive,
|
||||
)
|
||||
if isinstance(path, list):
|
||||
return [self._path(sub_path) for sub_path in path]
|
||||
|
||||
return os.fspath(self._root / path.lstrip("/"))
|
||||
|
||||
def create_dir(self, path):
|
||||
return self._fs.create_dir(self._path(path))
|
||||
def create_dir(self, path, **kwargs):
|
||||
return self._fs.create_dir(self._path(path), **kwargs)
|
||||
|
||||
def open_input_stream(self, path):
|
||||
return self._fs.open_input_stream(self._path(path))
|
||||
def open_input_stream(self, path, **kwargs):
|
||||
return self._fs.open_input_stream(self._path(path), **kwargs)
|
||||
|
||||
def open_output_stream(self, path):
|
||||
def open_output_stream(self, path, **kwargs):
|
||||
import posixpath
|
||||
|
||||
# NOTE: HadoopFileSystem.open_output_stream creates directories
|
||||
# automatically.
|
||||
self.create_dir(posixpath.dirname(path))
|
||||
return self._fs.open_output_stream(self._path(path))
|
||||
return self._fs.open_output_stream(self._path(path), **kwargs)
|
||||
|
||||
def get_file_info(self, path):
|
||||
return self._fs.get_file_info(self._path(path))
|
||||
def get_file_info(self, path, **kwargs):
|
||||
from pyarrow.fs import FileInfo
|
||||
|
||||
entries = self._fs.get_file_info(self._path(path), **kwargs)
|
||||
if isinstance(entries, FileInfo):
|
||||
return self._adjust_entry(entries)
|
||||
|
||||
assert isinstance(entries, list)
|
||||
return list(map(self._adjust_entry, entries))
|
||||
|
||||
def _adjust_entry(self, entry):
|
||||
import posixpath
|
||||
|
||||
from pyarrow.fs import FileInfo
|
||||
|
||||
mocked_path = os.path.relpath(entry.path, self._root)
|
||||
mocked_parts = mocked_path.split(os.path.sep)
|
||||
return FileInfo(
|
||||
path=posixpath.join(*mocked_parts),
|
||||
type=entry.type,
|
||||
mtime=entry.mtime,
|
||||
size=entry.size,
|
||||
)
|
||||
|
||||
def move(self, from_path, to_path):
|
||||
self._fs.move(self._path(from_path), self._path(to_path))
|
||||
@@ -259,7 +289,7 @@ def hdfs(test_config, mocker):
|
||||
"pyarrow.fs.HadoopFileSystem", FakeHadoopFileSystem, create=True
|
||||
)
|
||||
|
||||
mocker.patch("dvc.fs.hdfs._hadoop_fs_checksum", hadoop_fs_checksum)
|
||||
mocker.patch("dvc.fs.hdfs.HDFSFileSystem._checksum", hadoop_fs_checksum)
|
||||
|
||||
url = f"hdfs://example.com:12345/{uuid.uuid4()}"
|
||||
yield HDFS(url)
|
||||
|
||||
+41
-73
@@ -1,16 +1,13 @@
|
||||
import locale
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
|
||||
import mockssh
|
||||
import pytest
|
||||
from funcy import cached_property
|
||||
from mockssh.server import Handler
|
||||
|
||||
from dvc.path_info import URLInfo
|
||||
|
||||
from .base import Base
|
||||
from .local import Local
|
||||
|
||||
TEST_SSH_USER = "user"
|
||||
TEST_SSH_KEY_PATH = os.path.join(
|
||||
@@ -18,71 +15,18 @@ TEST_SSH_KEY_PATH = os.path.join(
|
||||
)
|
||||
|
||||
|
||||
# See: https://github.com/carletes/mock-ssh-server/issues/22
|
||||
class SSHMockHandler(Handler):
|
||||
def handle_client(self, channel):
|
||||
try:
|
||||
command = self.command_queues[channel.chanid].get(block=True)
|
||||
self.log.debug("Executing %s", command)
|
||||
if isinstance(command, bytes):
|
||||
command = command.decode()
|
||||
p = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = p.communicate()
|
||||
channel.sendall(stdout)
|
||||
channel.sendall_stderr(stderr)
|
||||
channel.send_exit_status(p.returncode)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
self.log.error(
|
||||
"Error handling client (channel: %s)", channel, exc_info=True
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
channel.close()
|
||||
except EOFError:
|
||||
self.log.debug("Tried to close already closed channel")
|
||||
|
||||
|
||||
class SSHMockServer(mockssh.Server):
|
||||
handler_cls = SSHMockHandler
|
||||
|
||||
|
||||
class SSHMocked(Base, URLInfo):
|
||||
class SSH(Base, URLInfo):
|
||||
@staticmethod
|
||||
def get_url(user, port): # pylint: disable=arguments-differ
|
||||
path = Local.get_storagepath()
|
||||
if os.name == "nt":
|
||||
# NOTE: On Windows Local.get_storagepath() will return an
|
||||
# ntpath that looks something like `C:\some\path`, which is not
|
||||
# compatible with SFTP paths [1], so we need to convert it to
|
||||
# a proper posixpath.
|
||||
# To do that, we should construct a posixpath that would be
|
||||
# relative to the server's root.
|
||||
# Our URL format requires absolute paths, so the
|
||||
# resulting path would look like `/some/path`.
|
||||
#
|
||||
# [1]https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-6
|
||||
drive, path = os.path.splitdrive(path)
|
||||
|
||||
# Hackish way to make sure SSH server runs on same drive as tests
|
||||
# and temporary directories are.
|
||||
# Context: https://github.com/iterative/dvc/pull/4660
|
||||
test_drive, _ = os.path.splitdrive(os.getcwd())
|
||||
if drive.lower() != test_drive.lower():
|
||||
raise Exception("Did you forget to use `tmp_dir?`")
|
||||
|
||||
path = path.replace("\\", "/")
|
||||
url = f"ssh://{user}@127.0.0.1:{port}{path}"
|
||||
return url
|
||||
def get_url(host, port): # pylint: disable=arguments-differ
|
||||
return f"ssh://{host}:{port}/tmp/data/{uuid.uuid4()}"
|
||||
|
||||
@cached_property
|
||||
def config(self):
|
||||
return {"url": self.url, "keyfile": TEST_SSH_KEY_PATH}
|
||||
return {
|
||||
"url": self.url,
|
||||
"user": TEST_SSH_USER,
|
||||
"keyfile": TEST_SSH_KEY_PATH,
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def _ssh(self):
|
||||
@@ -126,12 +70,34 @@ class SSHMocked(Base, URLInfo):
|
||||
return self.read_bytes().decode(encoding)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ssh_server(test_config):
|
||||
@pytest.fixture(scope="session")
|
||||
def ssh_server(test_config, docker_compose, docker_services):
|
||||
import asyncssh
|
||||
from sshfs import SSHFileSystem
|
||||
|
||||
test_config.requires("ssh")
|
||||
users = {TEST_SSH_USER: TEST_SSH_KEY_PATH}
|
||||
with SSHMockServer(users) as s:
|
||||
yield s
|
||||
conn_info = {
|
||||
"host": "127.0.0.1",
|
||||
"port": docker_services.port_for("openssh-server", 2222),
|
||||
}
|
||||
|
||||
def get_fs():
|
||||
return SSHFileSystem(
|
||||
**conn_info,
|
||||
username=TEST_SSH_USER,
|
||||
client_keys=[TEST_SSH_KEY_PATH],
|
||||
)
|
||||
|
||||
def _check():
|
||||
try:
|
||||
get_fs().exists("/")
|
||||
except asyncssh.Error:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
docker_services.wait_until_responsive(timeout=30.0, pause=1, check=_check)
|
||||
return conn_info
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -139,8 +105,8 @@ def ssh_connection(ssh_server):
|
||||
from sshfs import SSHFileSystem
|
||||
|
||||
yield SSHFileSystem(
|
||||
host=ssh_server.host,
|
||||
port=ssh_server.port,
|
||||
host=ssh_server["host"],
|
||||
port=ssh_server["port"],
|
||||
username=TEST_SSH_USER,
|
||||
client_files=[TEST_SSH_KEY_PATH],
|
||||
)
|
||||
@@ -153,4 +119,6 @@ def ssh(ssh_server, monkeypatch):
|
||||
# NOTE: see http://github.com/iterative/dvc/pull/3501
|
||||
monkeypatch.setattr(SSHFileSystem, "CAN_TRAVERSE", False)
|
||||
|
||||
return SSHMocked(SSHMocked.get_url(TEST_SSH_USER, ssh_server.port))
|
||||
url = SSH(SSH.get_url(**ssh_server))
|
||||
url.mkdir(exist_ok=True, parents=True)
|
||||
return url
|
||||
|
||||
+4
-153
@@ -1,15 +1,12 @@
|
||||
import locale
|
||||
import os
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.path_info import CloudURLInfo
|
||||
|
||||
from .base import Base
|
||||
from .hdfs import _hdfs_root, md5md5crc32c
|
||||
|
||||
|
||||
class WebHDFS(Base, CloudURLInfo): # pylint: disable=abstract-method
|
||||
@@ -17,7 +14,9 @@ class WebHDFS(Base, CloudURLInfo): # pylint: disable=abstract-method
|
||||
def _webhdfs(self):
|
||||
from hdfs import InsecureClient
|
||||
|
||||
client = InsecureClient(f"http://{self.host}:{self.port}", self.user)
|
||||
client = InsecureClient(
|
||||
f"http://{self.host}:{self.port}", self.user, root="/"
|
||||
)
|
||||
yield client
|
||||
|
||||
def is_file(self):
|
||||
@@ -58,155 +57,7 @@ class WebHDFS(Base, CloudURLInfo): # pylint: disable=abstract-method
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def real_webhdfs(hdfs_server):
|
||||
def webhdfs(hdfs_server):
|
||||
port = hdfs_server["webhdfs"]
|
||||
url = f"webhdfs://127.0.0.1:{port}/{uuid.uuid4()}"
|
||||
yield WebHDFS(url)
|
||||
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._root = Path(_hdfs_root.name)
|
||||
|
||||
def _path(self, path):
|
||||
return self._root / path.lstrip("/")
|
||||
|
||||
def makedirs(self, path, permission=None):
|
||||
self._path(path).mkdir(
|
||||
mode=permission or 0o777, exist_ok=True, parents=True
|
||||
)
|
||||
|
||||
def write(self, hdfs_path, overwrite=False):
|
||||
from hdfs.util import HdfsError
|
||||
|
||||
path = self._path(hdfs_path)
|
||||
|
||||
if not overwrite and path.exists():
|
||||
raise HdfsError(f"Remote path {hdfs_path} already exists.")
|
||||
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
return path.open("wb")
|
||||
|
||||
@contextmanager
|
||||
def read(
|
||||
self,
|
||||
hdfs_path,
|
||||
encoding=None,
|
||||
chunk_size=0,
|
||||
delimiter=None,
|
||||
progress=None,
|
||||
):
|
||||
pobj = self._path(hdfs_path)
|
||||
if not chunk_size and not delimiter:
|
||||
if encoding:
|
||||
yield pobj.open("r", encoding=encoding)
|
||||
else:
|
||||
yield pobj.open("rb")
|
||||
else:
|
||||
if delimiter:
|
||||
data = pobj.open("r", encoding=encoding, newline=delimiter)
|
||||
else:
|
||||
|
||||
def read_chunks(fobj, _chunk_size):
|
||||
while True:
|
||||
chunk = fobj.read(_chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
yield chunk
|
||||
|
||||
data = read_chunks(
|
||||
pobj.open("rb", encoding=encoding), chunk_size
|
||||
)
|
||||
|
||||
if progress:
|
||||
|
||||
def reader(_hdfs_path, _progress):
|
||||
nbytes = 0
|
||||
for chunk in data:
|
||||
nbytes += len(chunk)
|
||||
_progress(_hdfs_path, nbytes)
|
||||
yield chunk
|
||||
_progress(_hdfs_path, -1)
|
||||
|
||||
yield reader(hdfs_path, progress)
|
||||
else:
|
||||
yield data
|
||||
|
||||
def walk(self, hdfs_path):
|
||||
import posixpath
|
||||
|
||||
local_path = self._path(hdfs_path)
|
||||
for local_root, dnames, fnames in os.walk(local_path):
|
||||
if local_root == os.fspath(local_path):
|
||||
root = hdfs_path
|
||||
else:
|
||||
root = posixpath.join(
|
||||
hdfs_path, os.path.relpath(local_root, local_path)
|
||||
)
|
||||
yield (root, dnames, fnames)
|
||||
|
||||
def delete(self, hdfs_path):
|
||||
return self._path(hdfs_path).unlink()
|
||||
|
||||
def status(self, hdfs_path, strict=True):
|
||||
from hdfs.util import HdfsError
|
||||
|
||||
try:
|
||||
return {"length": self._path(hdfs_path).stat().st_size}
|
||||
except FileNotFoundError:
|
||||
if not strict:
|
||||
return None
|
||||
raise HdfsError(
|
||||
f"File does not exist: {hdfs_path}",
|
||||
exception="FileNotFoundException",
|
||||
)
|
||||
|
||||
def checksum(self, hdfs_path):
|
||||
return {
|
||||
"algorithm": "MD5-of-0MD5-of-512CRC32",
|
||||
"bytes": md5md5crc32c(self._path(hdfs_path)) + "00000000",
|
||||
"size": 28,
|
||||
}
|
||||
|
||||
def rename(self, from_path, to_path):
|
||||
from dvc.utils.fs import move
|
||||
|
||||
move(self._path(from_path), self._path(to_path))
|
||||
|
||||
def upload(
|
||||
self,
|
||||
hdfs_path,
|
||||
local_path,
|
||||
chunk_size=2 ** 16,
|
||||
progress=None,
|
||||
**kwargs,
|
||||
):
|
||||
with open(local_path, "rb") as from_fobj:
|
||||
with self.write(hdfs_path, **kwargs) as to_fobj:
|
||||
nbytes = 0
|
||||
while True:
|
||||
chunk = from_fobj.read(chunk_size)
|
||||
if not chunk:
|
||||
break
|
||||
if progress:
|
||||
nbytes += len(chunk)
|
||||
progress(local_path, nbytes)
|
||||
to_fobj.write(chunk)
|
||||
|
||||
def download(self, hdfs_path, local_path, **kwargs):
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
kwargs.setdefault("chunk_size", 2 ** 16)
|
||||
|
||||
makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
with open(local_path, "wb") as writer:
|
||||
with self.read(hdfs_path, **kwargs) as reader:
|
||||
for chunk in reader:
|
||||
writer.write(chunk)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def webhdfs(mocker):
|
||||
mocker.patch("hdfs.InsecureClient", FakeClient)
|
||||
url = f"webhdfs://example.com:12345/{uuid.uuid4()}"
|
||||
yield WebHDFS(url)
|
||||
|
||||
@@ -57,7 +57,7 @@ def test_list_outputs_only(mocker):
|
||||
|
||||
|
||||
def test_show_json(mocker, capsys):
|
||||
cli_args = parse_args(["list", "local_dir", "--show-json"])
|
||||
cli_args = parse_args(["list", "local_dir", "--json"])
|
||||
assert cli_args.func == CmdList
|
||||
|
||||
cmd = cli_args.func(cli_args)
|
||||
@@ -109,3 +109,8 @@ def test_show_colors(mocker, capsys, monkeypatch):
|
||||
"\x1b[01;34msrc\x1b[0m",
|
||||
"\x1b[01;32mrun.sh\x1b[0m",
|
||||
]
|
||||
|
||||
|
||||
def test_list_alias():
|
||||
cli_args = parse_args(["ls", "local_dir"])
|
||||
assert cli_args.func == CmdList
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from itertools import takewhile
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc.cli import parse_args
|
||||
|
||||
|
||||
def _id_gen(val) -> str:
|
||||
if isinstance(val, list):
|
||||
return "-".join(takewhile(lambda v: not v.startswith("-"), val))
|
||||
return str(val)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, key",
|
||||
[
|
||||
(["diff", "--show-json"], "json"),
|
||||
(["diff", "--show-md"], "markdown"),
|
||||
(["experiments", "diff", "--show-json"], "json"),
|
||||
(["experiments", "diff", "--show-md"], "markdown"),
|
||||
(["experiments", "show", "--show-json"], "json"),
|
||||
(["experiments", "show", "--show-csv"], "csv"),
|
||||
(["ls", "--show-json", "."], "json"),
|
||||
(["metrics", "diff", "--show-json"], "json"),
|
||||
(["metrics", "diff", "--show-md"], "markdown"),
|
||||
(["metrics", "show", "--show-json"], "json"),
|
||||
(["metrics", "show", "--show-md"], "markdown"),
|
||||
(["params", "diff", "--show-json"], "json"),
|
||||
(["params", "diff", "--show-md"], "markdown"),
|
||||
(["status", "--show-json"], "json"),
|
||||
],
|
||||
ids=_id_gen,
|
||||
)
|
||||
def test_backward_compat_flags(args, key):
|
||||
"""Test support for --show-csv/--show-json/--show-md flags."""
|
||||
cli_args = parse_args(args)
|
||||
d = vars(cli_args)
|
||||
assert d[key] is True
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import pytest
|
||||
|
||||
from dvc.cli import parse_args
|
||||
from dvc.command.diff import _digest, _show_md
|
||||
from dvc.command.diff import _digest, _show_markdown
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -102,7 +102,7 @@ def test_show_hash(mocker, capsys):
|
||||
|
||||
|
||||
def test_show_json(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json"])
|
||||
args = parse_args(["diff", "--json"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
"added": [
|
||||
@@ -124,7 +124,7 @@ def test_show_json(mocker, capsys):
|
||||
|
||||
|
||||
def test_show_json_and_hash(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json", "--show-hash"])
|
||||
args = parse_args(["diff", "--json", "--show-hash"])
|
||||
cmd = args.func(args)
|
||||
|
||||
diff = {
|
||||
@@ -161,7 +161,7 @@ def test_show_json_and_hash(mocker, capsys):
|
||||
|
||||
|
||||
def test_show_json_hide_missing(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json", "--hide-missing"])
|
||||
args = parse_args(["diff", "--json", "--hide-missing"])
|
||||
cmd = args.func(args)
|
||||
diff = {
|
||||
"added": [
|
||||
@@ -190,22 +190,22 @@ def test_show_json_hide_missing(mocker, capsys):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("show_hash", [None, True, False])
|
||||
def test_diff_show_md_and_hash(mocker, show_hash):
|
||||
options = ["diff", "--show-md"] + (["--show-hash"] if show_hash else [])
|
||||
def test_diff_show_markdown_and_hash(mocker, show_hash):
|
||||
options = ["diff", "--md"] + (["--show-hash"] if show_hash else [])
|
||||
args = parse_args(options)
|
||||
cmd = args.func(args)
|
||||
|
||||
diff = {}
|
||||
show_hash = show_hash if show_hash else False
|
||||
mock_show_md = mocker.patch("dvc.command.diff._show_md")
|
||||
mock_show_markdown = mocker.patch("dvc.command.diff._show_markdown")
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value=diff.copy())
|
||||
|
||||
assert 0 == cmd.run()
|
||||
mock_show_md.assert_called_once_with(diff, show_hash, False)
|
||||
mock_show_markdown.assert_called_once_with(diff, show_hash, False)
|
||||
|
||||
|
||||
def test_no_changes(mocker, capsys):
|
||||
args = parse_args(["diff", "--show-json"])
|
||||
args = parse_args(["diff", "--json"])
|
||||
cmd = args.func(args)
|
||||
mocker.patch("dvc.repo.Repo.diff", return_value={})
|
||||
|
||||
@@ -220,7 +220,7 @@ def test_no_changes(mocker, capsys):
|
||||
assert not out
|
||||
|
||||
|
||||
def test_show_md(capsys):
|
||||
def test_show_markdown(capsys):
|
||||
diff = {
|
||||
"deleted": [
|
||||
{"path": "zoo"},
|
||||
@@ -234,7 +234,7 @@ def test_show_md(capsys):
|
||||
"not in cache": [{"path": "file2"}],
|
||||
}
|
||||
|
||||
_show_md(diff)
|
||||
_show_markdown(diff)
|
||||
out, _ = capsys.readouterr()
|
||||
assert out == (
|
||||
"| Status | Path |\n"
|
||||
@@ -251,7 +251,7 @@ def test_show_md(capsys):
|
||||
).format(sep=os.path.sep)
|
||||
|
||||
|
||||
def test_show_md_with_hash(capsys):
|
||||
def test_show_markdown_with_hash(capsys):
|
||||
diff = {
|
||||
"deleted": [
|
||||
{"path": "zoo", "hash": "22222"},
|
||||
@@ -272,7 +272,7 @@ def test_show_md_with_hash(capsys):
|
||||
"not in cache": [{"path": "file2", "hash": "12345678"}],
|
||||
}
|
||||
|
||||
_show_md(diff, show_hash=True)
|
||||
_show_markdown(diff, show_hash=True)
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert out == (
|
||||
@@ -290,7 +290,7 @@ def test_show_md_with_hash(capsys):
|
||||
).format(sep=os.path.sep)
|
||||
|
||||
|
||||
def test_show_md_hide_missing(capsys):
|
||||
def test_show_markdown_hide_missing(capsys):
|
||||
diff = {
|
||||
"deleted": [
|
||||
{"path": "zoo"},
|
||||
@@ -304,7 +304,7 @@ def test_show_md_hide_missing(capsys):
|
||||
"not in cache": [{"path": "file2"}],
|
||||
}
|
||||
|
||||
_show_md(diff, hide_missing=True)
|
||||
_show_markdown(diff, hide_missing=True)
|
||||
|
||||
out, _ = capsys.readouterr()
|
||||
assert out == (
|
||||
|
||||
@@ -42,8 +42,8 @@ def test_experiments_diff(dvc, scm, mocker):
|
||||
"HEAD~1",
|
||||
"--all",
|
||||
"--param-deps",
|
||||
"--show-json",
|
||||
"--show-md",
|
||||
"--json",
|
||||
"--md",
|
||||
"--old",
|
||||
"--precision",
|
||||
"10",
|
||||
@@ -381,7 +381,7 @@ all_experiments = {
|
||||
|
||||
def test_show_experiments(capsys):
|
||||
show_experiments(
|
||||
all_experiments, precision=None, fill_value="", iso=True, show_csv=True
|
||||
all_experiments, precision=None, fill_value="", iso=True, csv=True
|
||||
)
|
||||
cap = capsys.readouterr()
|
||||
assert (
|
||||
@@ -405,3 +405,23 @@ def test_show_experiments(capsys):
|
||||
"3000,1,0.5843640011189556,0.9544670443829399,3000,1,20170428,100,36"
|
||||
in cap.out
|
||||
)
|
||||
|
||||
|
||||
def test_experiments_init_config(dvc, mocker):
|
||||
with dvc.config.edit() as conf:
|
||||
conf["exp"] = {"code": "new_src", "models": "new_models"}
|
||||
|
||||
m = mocker.patch("dvc.repo.experiments.init.init")
|
||||
cli_args = parse_args(["exp", "init", "cmd"])
|
||||
cmd = cli_args.func(cli_args)
|
||||
|
||||
assert cmd.run() == 0
|
||||
assert m.call_args[1]["defaults"] == {
|
||||
"code": "new_src",
|
||||
"models": "new_models",
|
||||
"data": "data",
|
||||
"metrics": "metrics.json",
|
||||
"params": "params.yaml",
|
||||
"plots": "plots",
|
||||
"live": "dvclive",
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ def test_metrics_diff(dvc, mocker, capsys):
|
||||
"HEAD~1",
|
||||
"-R",
|
||||
"--all",
|
||||
"--show-md",
|
||||
"--md",
|
||||
"--targets",
|
||||
"target1",
|
||||
"target2",
|
||||
@@ -59,7 +59,7 @@ def test_metrics_diff_json(dvc, mocker, capsys):
|
||||
"HEAD~1",
|
||||
"-R",
|
||||
"--all",
|
||||
"--show-json",
|
||||
"--json",
|
||||
"--targets",
|
||||
"target1",
|
||||
"target2",
|
||||
@@ -139,7 +139,7 @@ def test_metrics_show_json(dvc, mocker, capsys):
|
||||
[
|
||||
"metrics",
|
||||
"show",
|
||||
"--show-json",
|
||||
"--json",
|
||||
"-R",
|
||||
"--all-tags",
|
||||
"--all-branches",
|
||||
|
||||
@@ -12,8 +12,8 @@ def test_params_diff(dvc, mocker):
|
||||
"--targets",
|
||||
"target",
|
||||
"--all",
|
||||
"--show-json",
|
||||
"--show-md",
|
||||
"--json",
|
||||
"--md",
|
||||
"--no-path",
|
||||
"--deps",
|
||||
]
|
||||
@@ -61,9 +61,7 @@ def test_params_diff_from_cli(dvc, mocker):
|
||||
|
||||
|
||||
def test_params_diff_show_json(dvc, mocker, capsys):
|
||||
cli_args = parse_args(
|
||||
["params", "diff", "HEAD~10", "HEAD~1", "--show-json"]
|
||||
)
|
||||
cli_args = parse_args(["params", "diff", "HEAD~10", "HEAD~1", "--json"])
|
||||
cmd = cli_args.func(cli_args)
|
||||
mocker.patch(
|
||||
"dvc.repo.params.diff.diff", return_value={"params.yaml": {"a": "b"}}
|
||||
|
||||
@@ -46,7 +46,7 @@ def test_cloud_status(tmp_dir, dvc, mocker):
|
||||
|
||||
@pytest.mark.parametrize("status", [{}, {"a": "b", "c": [1, 2, 3]}, [1, 2, 3]])
|
||||
def test_status_show_json(dvc, mocker, capsys, status):
|
||||
cli_args = parse_args(["status", "--show-json"])
|
||||
cli_args = parse_args(["status", "--json"])
|
||||
assert cli_args.func == CmdDataStatus
|
||||
|
||||
cmd = cli_args.func(cli_args)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import os
|
||||
|
||||
from dvc.fs.hdfs import _hadoop_fs_checksum
|
||||
from dvc.path_info import URLInfo
|
||||
from dvc.utils import fix_env
|
||||
|
||||
|
||||
def test_hadoop_fs_checksum(mocker):
|
||||
mock_proc = mocker.Mock()
|
||||
|
||||
out = b"/path/to/file\tMD5-of-0MD5-of-512CRC32C\t123456789"
|
||||
err = b""
|
||||
mock_proc.configure_mock(
|
||||
**{"communicate.return_value": (out, err), "returncode": 0}
|
||||
)
|
||||
mock_popen = mocker.patch("subprocess.Popen", return_value=mock_proc)
|
||||
|
||||
path_info = URLInfo("hdfs://example.com:1234/path/to/file")
|
||||
|
||||
assert _hadoop_fs_checksum(path_info) == "123456789"
|
||||
mock_popen.assert_called_once_with(
|
||||
"hadoop fs -checksum hdfs://example.com:1234/path/to/file",
|
||||
shell=True,
|
||||
close_fds=os.name != "nt",
|
||||
executable=os.getenv("SHELL") if os.name != "nt" else None,
|
||||
env=fix_env(os.environ),
|
||||
stdin=-1,
|
||||
stdout=-1,
|
||||
stderr=-1,
|
||||
)
|
||||
assert mock_proc.communicate.called
|
||||
@@ -178,3 +178,23 @@ def test_ssh_multi_identity_files(mock_file):
|
||||
def test_ssh_gss_auth(mock_file, config, expected_gss_auth):
|
||||
fs = SSHFileSystem(**config)
|
||||
assert fs.fs_args["gss_auth"] == expected_gss_auth
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url, path",
|
||||
[
|
||||
("ssh://a@b/", "/"),
|
||||
("ssh://a@b:/", "/"),
|
||||
("ssh://a@b/home", "/home"),
|
||||
("ssh://a@b:/home", "/home"),
|
||||
("ssh://a@b/home/tmp", "/home/tmp"),
|
||||
("ssh://a@b:/home/tmp", "/home/tmp"),
|
||||
("ssh://a@b/home/tmp/something", "/home/tmp/something"),
|
||||
("ssh://a@b:/home/tmp/something", "/home/tmp/something"),
|
||||
],
|
||||
)
|
||||
def test_ssh_fs_path_management(url, path):
|
||||
fs = SSHFileSystem(host="x.y.z")
|
||||
|
||||
path_info = fs.PATH_CLS(url)
|
||||
assert fs._with_bucket(path_info) == path
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user