Compare commits

...
50 Commits
Author SHA1 Message Date
skshetry d762372d55 schema: add strict schema validation for top-level plots (#8289)
schema: add strict schema validation for plots
2022-09-15 16:30:10 +05:45
Peter Rowlands f3d304580a schema: update lockfile schema for versioning and dir files 2022-09-15 13:13:31 +03:00
Peter Rowlands 0e0d46e232 output: lazy load tree from files list in get_obj 2022-09-15 13:13:31 +03:00
Peter Rowlands 95ae018ed8 push/fetch: support dirs in worktree remotes 2022-09-15 13:13:31 +03:00
Peter Rowlands c29e721823 output: support loading/dumping dir trees from metadata 2022-09-15 13:13:31 +03:00
Peter Rowlands a9c7032d50 dvc-data: bump to 0.8.0 2022-09-15 13:13:31 +03:00
Peter Rowlands ecb767d622 index: add workspace/key generation helper 2022-09-15 13:13:31 +03:00
Saugat Pachhai (सौगात) fa45a9bbba dvcfs: default open to binary mode 2022-09-15 13:11:52 +03:00
daniele f50ae589ba deps: remove unused mock dep (#8290) 2022-09-15 07:34:21 +05:45
Daniele Trifirò 611c114f01 import-url: pass fs_config down from imp_url to get_cloud_fs 2022-09-14 22:26:19 +03:00
skshetry fdc18d5483 expose dvcfs in dvc.api and add to fsspec's registry (#8287)
* expose dvcfs in dvc.api and add to fsspec's registry

* readd __all__ to avoid exposing modules
2022-09-14 20:02:56 +05:45
David de la Iglesia Castro 3b532b7d59 exp run: Support hydra basic sweeper.
Requires `--queue` to be used alongside `--set-param`.

Doesn't strictly require `hydra.config.enabled`.

Add `get_hydra_sweeps` to `dvc/utils/hydra.py`.  Uses hydra `BasicSweeper` to generate the different sweeps.

`glob` syntax for sweeps is not supported in this P.R.

Closes #8083

---

Using setup from https://hydra.cc/docs/tutorials/basic/your_first_app/composition/ :

```console
$ dvc exp run -S "db=mysql,postgresql" -S "schema=warehouse,school" --queue
Queueing with '{'params.yaml': ['db=mysql', 'schema=warehouse']}'.
Queued experiment '5ab98b8' for future execution.
Queueing with '{'params.yaml': ['db=mysql', 'schema=school']}'.
Queued experiment '57c2fb6' for future execution.
Queueing with '{'params.yaml': ['db=postgresql', 'schema=warehouse']}'.
Queued experiment 'b9d6391' for future execution.
Queueing with '{'params.yaml': ['db=postgresql', 'schema=school']}'.
Queued experiment '145cd55' for future execution.
```
2022-09-13 13:43:30 +02:00
skshetry e78c4455de dvcfs: remove unused config code for opening a repo, change signature (#8277)
dvcfs: remove unused code for opening a repo
2022-09-13 09:26:33 +05:45
skshetry 8e5f4a8f35 data ls: new command to show metadata with outputs (#8252)
data ls: add new command to show metadata for the outputs
2022-09-09 17:36:24 +05:45
Saugat Pachhai (सौगात) cb3159e586 dvcfs: remove config 2022-09-09 13:16:27 +03:00
skshetry 7b301b09a1 data status: fix type hints (#8275) 2022-09-09 13:38:41 +05:45
skshetry 55b16166c8 add/import/import-url: update help message for labels/type/desc (#8271) 2022-09-08 23:09:10 +05:45
Ronan Lamy c70d0bd14d fs: cleanup and rename _get_fs_pair_2() to _get_subrepo_info() 2022-09-08 17:39:32 +03:00
Ronan Lamy b99a0cd6ca fs: _get_dvc_path() cannot be called with dvc_fs=None 2022-09-08 17:39:32 +03:00
Ronan Lamy d4170b2ec0 fs: refactor _get_fs_pair_2() 2022-09-08 17:39:32 +03:00
Ronan Lamy a3181bb509 fs: remove _get_fs_pair() 2022-09-08 17:39:32 +03:00
Ronan Lamy 4d6b90502b fs: improve performance of _DvcFileSystem._update()
Avoid back and forth conversions between key tuples and path strings.
2022-09-08 17:39:32 +03:00
Ronan Lamy 947427f4d9 fs: remove dead code 2022-09-08 17:39:32 +03:00
Ronan Lamy 0482a1a88b fs: simplify data flow
This uses the fact that repo.fs is always self.repo.fs and that
self.repo.fs always exists.
2022-09-08 17:39:32 +03:00
Ronan Lamy b470459987 fs: simplify return values of _get_fs_pair_2() 2022-09-08 17:39:32 +03:00
Ronan Lamy 81a010674e fs: avoid costly redundant dvcignore check when calling ._info() from .ls() 2022-09-08 17:39:32 +03:00
Ronan Lamy d946a1328e fs: Extract ._info() from .info()
This avoids unnecessarily recomputing dvc_parts when calling .info()
from .ls()
2022-09-08 17:39:32 +03:00
Ronan Lamy ad7d39da20 fs: chop up ._get_fs_pair() 2022-09-08 17:39:32 +03:00
Saugat Pachhai (सौगात) 8432de5980 metadata: add support for dvc.yaml file 2022-09-08 15:10:33 +03:00
Saugat Pachhai (सौगात) c73764156e metadata: support custom metadata in .dvc files
This adds support for custom metadata in outputs under `meta` key.
Also adds `--meta` flag to `add`/`import`/`import-url` that supports
`key=value` values.
2022-09-08 14:10:35 +03:00
skshetry df60df5242 add metadata fields: label, type to data (#8232)
gto like metadata fields: labels and type
2022-09-08 16:33:47 +05:45
dependabot[bot] 504485ed6e build(deps-dev): Bump pylint from 2.15.0 to 2.15.2 (#8268)
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.0 to 2.15.2.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.0...v2.15.2)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-08 10:57:12 +05:45
dependabot[bot] 1e989bde2c build(deps): Bump scmrepo from 0.1.0 to 0.1.1 (#8269)
Bumps [scmrepo](https://github.com/iterative/scmrepo) from 0.1.0 to 0.1.1.
- [Release notes](https://github.com/iterative/scmrepo/releases)
- [Commits](https://github.com/iterative/scmrepo/compare/0.1.0...0.1.1)

---
updated-dependencies:
- dependency-name: scmrepo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-08 10:56:53 +05:45
Ruslan Kuprieiev 7e2248bbe5 deps: bump dvc-data to 0.7.1 2022-09-07 23:07:35 +03:00
Daniele Trifirò a2d80b2403 silence flake8 errors (abstract class with no abstract method) 2022-09-07 21:51:33 +03:00
Daniele Trifirò 76e7c76ae5 data status: fix for scmrepo 0.1.0 2022-09-07 21:48:53 +03:00
Ruslan Kuprieiev 6cada6dc12 dvc: cloud versioning POC 2022-09-07 21:43:34 +03:00
Ruslan Kuprieiev 89d4461908 deps: temporarily limit dulwich to <0.20.46
Looks like the new release broke some data status logic.
2022-09-07 02:56:14 +03:00
Ruslan Kuprieiev a124d7e866 deps: bump dvc-data to 0.6.3 2022-09-07 02:23:29 +03:00
Ruslan Kuprieiev 628be2ae49 deps: bump dvc-data to 0.6.1 2022-09-06 20:30:20 +03:00
dependabot[bot] 7a71e06946 build(deps): Bump dvc-render from 0.0.9 to 0.0.10
Bumps [dvc-render](https://github.com/iterative/dvc-render) from 0.0.9 to 0.0.10.
- [Release notes](https://github.com/iterative/dvc-render/releases)
- [Commits](https://github.com/iterative/dvc-render/compare/0.0.9...0.0.10)

---
updated-dependencies:
- dependency-name: dvc-render
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-06 17:12:36 +02:00
dependabot[bot] 91e54d8c27 build(deps): Bump dvc-data from 0.4.0 to 0.5.3 2022-09-06 17:17:21 +03:00
skshetry 071b2b8f81 add support for foreach target (#8210)
add general support for foreach target
2022-09-06 06:39:37 +05:45
skshetry 0b2b59a601 dvc.yaml: preserve outputs' desc on rewrites/updates to the stage (#8247)
preserve outputs' desc on rewrites/updates to the stage

We were not loading `desc` of the output, due to which we were not preserving the desc field over rewrites. Note that the rewrite only happens in select commands for `dvc.yaml`: `freeze`/`unfreeze`/`run`/`stage-add`, so we did not notice this before.
2022-09-05 18:49:39 +05:45
dependabot[bot] 65dc1cd08f build(deps-dev): Bump pytest from 7.1.2 to 7.1.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.2 to 7.1.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.1.2...7.1.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-03 02:04:01 +03:00
dependabot[bot] 1b103297b7 build(deps-dev): Bump dvc-azure from 2.20.0 to 2.20.2
Bumps [dvc-azure](https://github.com/iterative/dvc-azure) from 2.20.0 to 2.20.2.
- [Release notes](https://github.com/iterative/dvc-azure/releases)
- [Commits](https://github.com/iterative/dvc-azure/compare/2.20.0...2.20.2)

---
updated-dependencies:
- dependency-name: dvc-azure
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2022-09-03 02:03:46 +03:00
John Henning 887c73b3c6 adjusted formatting for dvc-hdfs 2022-09-02 23:04:59 +03:00
Peter Rowlands 660c17f654 output: save version_id meta 2022-09-01 19:36:53 +03:00
skshetry 49da2eef02 tests: set celery ping_task_timeout 3x the default (#8221)
tests: set celery ping_timeout 3x the default
2022-09-01 16:48:19 +05:45
Ruslan Kuprieiev aff65d42bb deps: bump dvc-azure and dvc-s3 to 2.20.0 2022-09-01 13:00:47 +03:00
77 changed files with 1433 additions and 493 deletions
+35
View File
@@ -0,0 +1,35 @@
from dataclasses import asdict, dataclass, field, fields
from typing import Any, ClassVar, Dict, List, Optional
from funcy import compact
@dataclass
class Annotation:
PARAM_DESC: ClassVar[str] = "desc"
PARAM_TYPE: ClassVar[str] = "type"
PARAM_LABELS: ClassVar[str] = "labels"
PARAM_META: ClassVar[str] = "meta"
desc: Optional[str] = None
type: Optional[str] = None
labels: List[str] = field(default_factory=list)
meta: Dict[str, Any] = field(default_factory=dict)
def update(self, **kwargs) -> "Annotation":
for attr, value in kwargs.items():
if value and hasattr(self, attr):
setattr(self, attr, value)
return self
def to_dict(self) -> Dict[str, str]:
return compact(asdict(self))
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
ANNOTATION_SCHEMA = {
Annotation.PARAM_DESC: str,
Annotation.PARAM_TYPE: str,
Annotation.PARAM_LABELS: [str],
Annotation.PARAM_META: object,
}
+12 -6
View File
@@ -1,9 +1,15 @@
from .data import ( # noqa, pylint: disable=redefined-builtin
get_url,
open,
read,
)
from dvc.fs.dvc import _DvcFileSystem as DvcFileSystem
from .data import open # pylint: disable=redefined-builtin
from .data import get_url, read
from .experiments import make_checkpoint
from .params import params_show
__all__ = ["get_url", "make_checkpoint", "open", "params_show", "read"]
__all__ = [
"get_url",
"make_checkpoint",
"open",
"params_show",
"read",
"DvcFileSystem",
]
+1 -1
View File
@@ -121,7 +121,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
)
return None
from dvc_data.build import IgnoreInCollectedDirError
from dvc_data.hashfile.build import IgnoreInCollectedDirError
if isinstance(exc, IgnoreInCollectedDirError):
logger.exception("")
+20
View File
@@ -0,0 +1,20 @@
from argparse import _AppendAction
class CommaSeparatedArgs(_AppendAction): # pylint: disable=protected-access
def __call__(self, parser, namespace, values, option_string=None):
from funcy import ldistinct
items = getattr(namespace, self.dest) or []
items.extend(map(str.strip, values.split(",")))
setattr(namespace, self.dest, ldistinct(items))
class KeyValueArgs(_AppendAction):
def __call__(self, parser, namespace, values, option_string=None):
items = getattr(namespace, self.dest) or {}
for value in filter(bool, values):
key, _, value = value.partition("=")
items[key.strip()] = value
setattr(namespace, self.dest, items)
+36 -9
View File
@@ -2,12 +2,43 @@ import argparse
import logging
from dvc.cli import completion
from dvc.cli.actions import KeyValueArgs
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
logger = logging.getLogger(__name__)
def _add_annotating_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help="User description of the data.",
)
parser.add_argument(
"--meta",
metavar="key=value",
nargs=1,
action=KeyValueArgs,
help="Custom metadata to add to the data.",
)
parser.add_argument(
"--label",
dest="labels",
type=str,
action="append",
metavar="<str>",
help="Label for the data.",
)
parser.add_argument(
"--type",
type=str,
metavar="<str>",
help="Type of the data.",
)
class CmdAdd(CmdBase):
def run(self):
from dvc.exceptions import (
@@ -28,6 +59,9 @@ class CmdAdd(CmdBase):
glob=self.args.glob,
desc=self.args.desc,
out=self.args.out,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
remote=self.args.remote,
to_remote=self.args.to_remote,
jobs=self.args.jobs,
@@ -109,15 +143,8 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help=(
"User description of the data (optional). "
"This doesn't affect any DVC operations."
),
)
_add_annotating_args(parser)
parser.add_argument(
"targets", nargs="+", help="Input files/directories to add."
).complete = completion.FILE
+111 -1
View File
@@ -1,9 +1,12 @@
import argparse
import logging
from typing import TYPE_CHECKING
from operator import itemgetter
from typing import TYPE_CHECKING, Any, Dict, Iterable, Set
from funcy import chunks, compact, log_durations
from dvc.cli import completion
from dvc.cli.actions import CommaSeparatedArgs
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link, fix_subparsers
from dvc.ui import ui
@@ -124,6 +127,65 @@ class CmdDataStatus(CmdBase):
return self._show_status(status)
class CmdDataLs(CmdBase):
@staticmethod
def _show_table(
d: Iterable[Dict[str, Any]],
filter_types: Set[str],
filter_labels: Set[str],
markdown: bool = False,
) -> None:
from rich.style import Style
from dvc.compare import TabularData
td = TabularData(
columns=["Path", "Type", "Labels", "Description"], fill_value="-"
)
for entry in sorted(d, key=itemgetter("path")):
typ = entry.get("type", "")
desc = entry.get("desc", "-")
labels = entry.get("labels", [])
if filter_types and typ not in filter_types:
continue
if filter_labels and filter_labels.isdisjoint(labels):
continue
rich_label = ui.rich_text()
for index, label in enumerate(labels):
if index:
rich_label.append(",")
style = Style(bold=label in filter_labels, color="green")
rich_label.append(label, style=style)
if markdown and desc:
desc = desc.partition("\n")[0]
path = ui.rich_text(entry["path"], style="cyan")
type_style = Style(bold=typ in filter_types, color="yellow")
typ = ui.rich_text(entry.get("type", ""), style=type_style)
td.append([path, typ or "-", rich_label or "-", desc])
td.render(markdown=markdown, rich_table=True)
def run(self):
from dvc.repo.data import ls
filter_labels = set(self.args.labels)
filter_types = set(self.args.type)
d = ls(
self.repo, targets=self.args.targets, recursive=self.args.recursive
)
self._show_table(
d,
filter_labels=filter_labels,
filter_types=filter_types,
markdown=self.args.markdown,
)
return 0
def add_parser(subparsers, parent_parser):
data_parser = subparsers.add_parser(
"data",
@@ -181,3 +243,51 @@ def add_parser(subparsers, parent_parser):
help="Show untracked files.",
)
data_status_parser.set_defaults(func=CmdDataStatus)
DATA_LS_HELP = "List data tracked by DVC with its metadata."
data_ls_parser = data_subparsers.add_parser(
"ls",
aliases=["list"],
parents=[parent_parser],
description=append_doc_link(DATA_LS_HELP, "data/ls"),
formatter_class=argparse.RawDescriptionHelpFormatter,
help=DATA_LS_HELP,
)
data_ls_parser.add_argument(
"--md",
"--show-md",
dest="markdown",
action="store_true",
default=False,
help="Show tabulated output in the Markdown format (GFM).",
)
data_ls_parser.add_argument(
"--type",
action=CommaSeparatedArgs,
default=[],
help="Comma-separated list of type to filter.",
)
data_ls_parser.add_argument(
"--labels",
action=CommaSeparatedArgs,
default=[],
help="Comma-separated list of labels to filter.",
)
data_ls_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Recursively list from the specified directories.",
)
data_ls_parser.add_argument(
"targets",
default=None,
nargs="*",
help=(
"Limit command scope to these tracked files/directories, "
".dvc files, or stage names."
),
).complete = completion.DVCFILES_AND_STAGE
data_ls_parser.set_defaults(func=CmdDataLs)
+7 -9
View File
@@ -21,6 +21,9 @@ class CmdImport(CmdBase):
no_exec=self.args.no_exec,
no_download=self.args.no_download,
desc=self.args.desc,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
)
except DvcException:
@@ -34,6 +37,8 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
from .add import _add_annotating_args
IMPORT_HELP = (
"Download file or directory tracked by DVC or by Git "
"into the workspace, and track it."
@@ -84,15 +89,6 @@ def add_parser(subparsers, parent_parser):
help="Create .dvc file including target data hash value(s)"
" but do not actually download the file(s).",
)
import_parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help=(
"User description of the data (optional). "
"This doesn't affect any DVC operations."
),
)
import_parser.add_argument(
"-j",
"--jobs",
@@ -103,4 +99,6 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImport)
+6 -9
View File
@@ -21,6 +21,9 @@ class CmdImportUrl(CmdBase):
remote=self.args.remote,
to_remote=self.args.to_remote,
desc=self.args.desc,
type=self.args.type,
labels=self.args.labels,
meta=self.args.meta,
jobs=self.args.jobs,
)
except DvcException:
@@ -35,6 +38,8 @@ class CmdImportUrl(CmdBase):
def add_parser(subparsers, parent_parser):
from .add import _add_annotating_args
IMPORT_HELP = (
"Download or copy file from URL and take it under DVC control."
)
@@ -103,13 +108,5 @@ def add_parser(subparsers, parent_parser):
),
metavar="<number>",
)
import_parser.add_argument(
"--desc",
type=str,
metavar="<text>",
help=(
"User description of the data (optional). "
"This doesn't affect any DVC operations."
),
)
_add_annotating_args(import_parser)
import_parser.set_defaults(func=CmdImportUrl)
+1 -1
View File
@@ -76,7 +76,7 @@ class CmdStageList(CmdBase):
# removing duplicates while maintaining order
collected = chain.from_iterable(
self.repo.stage.collect(
target=target, recursive=self.args.recursive, accept_group=True
target=target, recursive=self.args.recursive
)
for target in self.args.targets
)
+2
View File
@@ -78,7 +78,9 @@ REMOTE_COMMON = {
"url": str,
"checksum_jobs": All(Coerce(int), Range(1)),
"jobs": All(Coerce(int), Range(1)),
Optional("worktree"): Bool,
Optional("no_traverse"): Bool, # obsoleted
Optional("version_aware"): Bool,
}
LOCAL_COMMON = {
"type": supported_cache_type,
+40 -14
View File
@@ -3,16 +3,37 @@
import logging
from typing import TYPE_CHECKING, Iterable, Optional
from dvc_data.db import get_index
from funcy import cached_property
from dvc_data.hashfile.db import get_index
if TYPE_CHECKING:
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.status import CompareStatusResult
from dvc_data.hashfile.status import CompareStatusResult
logger = logging.getLogger(__name__)
class Remote:
def __init__(self, path, fs, **config):
self.path = path
self.fs = fs
self.worktree = config.pop("worktree", False)
self.config = config
@cached_property
def odb(self):
from dvc_data.hashfile.db import get_odb
path = self.path
if self.worktree:
path = self.fs.path.join(path, ".dvc", "cache")
return get_odb(self.fs, path, **self.config)
class DataCloud:
"""Class that manages dvc remotes.
@@ -27,18 +48,23 @@ class DataCloud:
def __init__(self, repo):
self.repo = repo
def get_remote_odb(
def get_remote(
self,
name: Optional[str] = None,
command: str = "<command>",
) -> "HashFileDB":
) -> "Remote":
from dvc.config import NoRemoteError
if not name:
name = self.repo.config["core"].get("remote")
if name:
return self._init_odb(name)
from dvc.fs import get_cloud_fs
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
fs = cls(**config)
config["tmp_dir"] = self.repo.index_db_dir
return Remote(fs_path, fs, **config)
if bool(self.repo.config["remote"]):
error_msg = (
@@ -55,13 +81,13 @@ class DataCloud:
raise NoRemoteError(error_msg)
def _init_odb(self, name):
from dvc.fs import get_cloud_fs
from dvc_data.db import get_odb
cls, config, fs_path = get_cloud_fs(self.repo, name=name)
config["tmp_dir"] = self.repo.index_db_dir
return get_odb(cls(**config), fs_path, **config)
def get_remote_odb(
self,
name: Optional[str] = None,
command: str = "<command>",
) -> "HashFileDB":
remote = self.get_remote(name=name, command=command)
return remote.odb
def _log_missing(self, status: "CompareStatusResult"):
if status.missing:
@@ -82,7 +108,7 @@ class DataCloud:
**kwargs,
):
from dvc.exceptions import FileTransferError
from dvc_data.transfer import TransferError, transfer
from dvc_data.hashfile.transfer import TransferError, transfer
try:
return transfer(src_odb, dest_odb, objs, **kwargs)
@@ -164,7 +190,7 @@ class DataCloud:
log_missing: log warning messages if file doesn't exist
neither in cache, neither in cloud.
"""
from dvc_data.status import compare_status
from dvc_data.hashfile.status import compare_status
if not odb:
odb = self.get_remote_odb(remote, "status")
+12 -4
View File
@@ -18,7 +18,7 @@ SCHEMA: Mapping[str, Any] = {
}
def _get(stage, p, info):
def _get(stage, p, info, fs_config=None):
if info and info.get(RepoDependency.PARAM_REPO):
repo = info.pop(RepoDependency.PARAM_REPO)
return RepoDependency(repo, stage, p, info)
@@ -27,7 +27,7 @@ def _get(stage, p, info):
params = info.pop(ParamsDependency.PARAM_PARAMS)
return ParamsDependency(stage, p, params)
return Dependency(stage, p, info)
return Dependency(stage, p, info, fs_config=fs_config)
def loadd_from(stage, d_list):
@@ -38,10 +38,18 @@ def loadd_from(stage, d_list):
return ret
def loads_from(stage, s_list, erepo=None):
def loads_from(stage, s_list, erepo=None, fs_config=None):
assert isinstance(s_list, list)
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
return [_get(stage, s, info.copy()) for s in s_list]
return [
_get(
stage,
s,
info.copy(),
fs_config=fs_config,
)
for s in s_list
]
def _merge_params(s_list):
+6 -5
View File
@@ -39,14 +39,15 @@ class ParamsDependency(Dependency):
def __init__(self, stage, path, params=None, repo=None):
self.params = list(params) if params else []
info = (
{self.PARAM_PARAMS: params} if isinstance(params, dict) else None
)
hash_info = HashInfo()
if isinstance(params, dict):
hash_info = HashInfo(self.PARAM_PARAMS, params)
repo = repo or stage.repo
path = path or os.path.join(repo.root_dir, self.DEFAULT_PARAMS_FILE)
super().__init__(stage, path, info=info, repo=repo)
super().__init__(stage, path, repo=repo)
self.hash_info = hash_info
def dumpd(self):
def dumpd(self, **kwargs):
ret = super().dumpd()
if not self.hash_info:
ret[self.PARAM_PARAMS] = self.params or {}
+6 -6
View File
@@ -59,11 +59,11 @@ class RepoDependency(Dependency):
def save(self):
pass
def dumpd(self):
def dumpd(self, **kwargs):
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
def download(self, to, jobs=None):
from dvc_data.checkout import checkout
from dvc_data.hashfile.checkout import checkout
for odb, objs in self.get_used_objs().items():
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
@@ -103,8 +103,8 @@ class RepoDependency(Dependency):
from dvc.config import NoRemoteError
from dvc.exceptions import NoOutputOrStageError, PathMissingError
from dvc.utils import as_posix
from dvc_data.build import build
from dvc_data.objects.tree import Tree, TreeError
from dvc_data.hashfile.build import build
from dvc_data.hashfile.tree import Tree, TreeError
local_odb = self.repo.odb.local
locked = kwargs.pop("locked", True)
@@ -153,8 +153,8 @@ class RepoDependency(Dependency):
def _check_circular_import(self, odb, obj_ids):
from dvc.exceptions import CircularImportError
from dvc.fs.dvc import DvcFileSystem
from dvc_data.db.reference import ReferenceHashFileDB
from dvc_data.objects.tree import Tree
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
from dvc_data.hashfile.tree import Tree
if not isinstance(odb, ReferenceHashFileDB):
return
+6 -3
View File
@@ -198,7 +198,7 @@ class SingleStageFile(FileMixin):
if self.verify:
check_dvcfile_path(self.repo, self.path)
logger.debug("Saving information to '%s'.", relpath(self.path))
dump_yaml(self.path, serialize.to_single_stage_file(stage))
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
self.repo.scm_context.track_file(self.relpath)
def remove_stage(self, stage): # pylint: disable=unused-argument
@@ -243,9 +243,11 @@ class PipelineFile(FileMixin):
self._lockfile.dump(stage)
@staticmethod
def _check_if_parametrized(stage):
def _check_if_parametrized(stage, action: str = "dump") -> None:
if stage.raw_data.parametrized:
raise ParametrizedDumpError(f"cannot dump a parametrized {stage}")
raise ParametrizedDumpError(
f"cannot {action} a parametrized {stage}"
)
def _dump_pipeline_file(self, stage):
self._check_if_parametrized(stage)
@@ -291,6 +293,7 @@ class PipelineFile(FileMixin):
self._lockfile.remove()
def remove_stage(self, stage):
self._check_if_parametrized(stage, "remove")
self._lockfile.remove_stage(stage)
if not self.exists():
return
+93 -182
View File
@@ -4,8 +4,7 @@ import os
import posixpath
import threading
from contextlib import suppress
from itertools import takewhile
from typing import TYPE_CHECKING, Callable, Optional, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple, Type, Union
from fsspec.spec import AbstractFileSystem
from funcy import cached_property, wrap_prop, wrap_with
@@ -29,11 +28,6 @@ def as_posix(path: str) -> str:
return path.replace(ntpath.sep, posixpath.sep)
def _wrap_walk(dvc_fs, *args, **kwargs):
for root, dnames, fnames in dvc_fs.walk(*args, **kwargs):
yield dvc_fs.path.join(dvc_fs.repo.root_dir, root), dnames, fnames
# NOT the same as dvc.dvcfile.is_dvc_file()!
def _is_dvc_file(fname):
from dvc.dvcfile import is_valid_filename
@@ -42,20 +36,6 @@ def _is_dvc_file(fname):
return is_valid_filename(fname) or fname == DvcIgnore.DVCIGNORE_FILE
def _ls(fs, path):
dnames = []
fnames = []
for entry in fs.ls(path, detail=True):
name = fs.path.name(entry["name"])
if entry["type"] == "directory":
dnames.append(name)
else:
fnames.append(name)
return dnames, fnames
def _merge_info(repo, fs_info, dvc_info):
from . import utils
@@ -79,6 +59,10 @@ def _merge_info(repo, fs_info, dvc_info):
return ret
def _get_dvc_path(dvc_fs, subkey):
return dvc_fs.path.join(*subkey) if subkey else ""
class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
"""DVC + git-tracked files fs.
@@ -91,28 +75,25 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
root_marker = "/"
PARAM_REPO_URL = "repo_url"
PARAM_REPO_ROOT = "repo_root"
PARAM_REV = "rev"
PARAM_CACHE_DIR = "cache_dir"
PARAM_CACHE_TYPES = "cache_types"
PARAM_SUBREPOS = "subrepos"
def __init__(
self,
url: Optional[str] = None,
rev: Optional[str] = None,
repo: Optional["Repo"] = None,
subrepos=False,
repo_factory: RepoFactory = None,
**kwargs,
):
super().__init__()
subrepos: bool = False,
repo_factory: Optional[RepoFactory] = None,
**repo_kwargs: Any,
) -> None:
from pygtrie import Trie
super().__init__()
if repo is None:
repo, repo_factory = self._repo_from_fs_config(
subrepos=subrepos, **kwargs
repo = self._make_repo(
url=url, rev=rev, subrepos=subrepos, **repo_kwargs
)
assert repo
# pylint: disable=protected-access
repo_factory = repo._fs_conf["repo_factory"]
if not repo_factory:
from dvc.repo import Repo
@@ -167,64 +148,12 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
return None
return self.repo.url
@property
def config(self):
return {
self.PARAM_REPO_URL: self.repo_url,
self.PARAM_REPO_ROOT: self.repo.root_dir,
self.PARAM_REV: getattr(self.repo.fs, "rev", None),
self.PARAM_CACHE_DIR: os.path.abspath(self.repo.odb.local.path),
self.PARAM_CACHE_TYPES: self.repo.odb.local.cache_types,
self.PARAM_SUBREPOS: self._traverse_subrepos,
}
@classmethod
def _repo_from_fs_config(
cls, **config
) -> Tuple["Repo", Optional["RepoFactory"]]:
from dvc.external_repo import erepo_factory, external_repo
def _make_repo(cls, **kwargs) -> "Repo":
from dvc.repo import Repo
url = config.get(cls.PARAM_REPO_URL)
root = config.get(cls.PARAM_REPO_ROOT)
assert url or root
def _open(*args, **kwargs):
# NOTE: if original repo was an erepo (and has a URL),
# we cannot use Repo.open() since it will skip erepo
# cache/remote setup for local URLs
if url is None:
return Repo.open(*args, **kwargs)
return external_repo(*args, **kwargs)
cache_dir = config.get(cls.PARAM_CACHE_DIR)
cache_config = (
{}
if not cache_dir
else {
"cache": {
"dir": cache_dir,
"type": config.get(cls.PARAM_CACHE_TYPES),
}
}
)
repo_kwargs: dict = {
"rev": config.get(cls.PARAM_REV),
"subrepos": config.get(cls.PARAM_SUBREPOS, False),
"uninitialized": True,
}
factory: Optional["RepoFactory"] = None
if url is None:
repo_kwargs["config"] = cache_config
else:
repo_kwargs["cache_dir"] = cache_dir
factory = erepo_factory(url, root, cache_config)
with _open(
url if url else root,
**repo_kwargs,
) as repo:
return repo, factory
with Repo.open(uninitialized=True, **kwargs) as repo:
return repo
def _get_repo(self, key: Key) -> "Repo":
"""Returns repo that the path falls in, using prefix.
@@ -239,24 +168,16 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
return repo
prefix_key, repo = self._subrepos_trie.longest_prefix(key)
prefix = self.repo.fs.path.join(
self.repo.root_dir,
*prefix_key, # pylint: disable=not-an-iterable
)
path = self._from_key(key)
parents = (parent for parent in self.repo.fs.path.parents(path))
dirs = [path] + list(takewhile(lambda p: p != prefix, parents))
dirs.reverse()
self._update(dirs, starting_repo=repo)
dir_keys = (key[:i] for i in range(len(prefix_key) + 1, len(key) + 1))
self._update(dir_keys, starting_repo=repo)
return self._subrepos_trie.get(key) or self.repo
@wrap_with(threading.Lock())
def _update(self, dirs, starting_repo):
def _update(self, dir_keys, starting_repo):
"""Checks for subrepo in directories and updates them."""
repo = starting_repo
for d in dirs:
key = self._get_key(d)
for key in dir_keys:
d = self._from_key(key)
if self._is_dvc_repo(d):
repo = self.repo_factory(
d,
@@ -279,75 +200,64 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
repo_path = self.repo.fs.path.join(dir_path, Repo.DVC_DIR)
return self.repo.fs.isdir(repo_path)
def _get_fs_pair(
self, path
) -> Tuple[
Optional["Repo"],
Optional[FileSystem],
Optional[str],
Optional[DataFileSystem],
Optional[str],
]:
def _get_subrepo_info(
self, key: Key
) -> Tuple["Repo", Optional[DataFileSystem], Key]:
"""
Returns a pair of fss based on repo the path falls in, using prefix.
Returns information about the subrepo the key is part of.
"""
key = self._get_key_from_relative(path)
fs_path = self._from_key(key)
repo = self._get_repo(key)
fs = repo.fs
repo_key: Key
if repo is self.repo:
dvc_parts = key
dvc_fs = self._datafss.get(())
repo_key = ()
subkey = key
else:
repo_parts = fs.path.relparts(repo.root_dir, self.repo.root_dir)
dvc_parts = key[len(repo_parts) :]
key = self._get_key(repo.root_dir)
dvc_fs = self._datafss.get(key)
repo_key = self._get_key(repo.root_dir)
subkey = key[len(repo_key) :]
if dvc_fs:
dvc_path = dvc_fs.path.join(*dvc_parts) if dvc_parts else ""
else:
dvc_path = None
dvc_fs = self._datafss.get(repo_key)
return repo, dvc_fs, subkey
return repo, fs, fs_path, dvc_fs, dvc_path
def open(
self, path, mode="r", encoding="utf-8", **kwargs
def _open(
self, path, mode="rb", **kwargs
): # pylint: disable=arguments-renamed, arguments-differ
if "b" in mode:
encoding = None
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
key = self._get_key_from_relative(path)
fs_path = self._from_key(key)
try:
return fs.open(fs_path, mode=mode, encoding=encoding)
return self.repo.fs.open(fs_path, mode=mode)
except FileNotFoundError:
_, dvc_fs, subkey = self._get_subrepo_info(key)
if not dvc_fs:
raise
return dvc_fs.open(dvc_path, mode=mode, encoding=encoding, **kwargs)
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs.open(dvc_path, mode=mode)
def isdvc(self, path, **kwargs):
_, _, _, dvc_fs, dvc_path = self._get_fs_pair(path)
key = self._get_key_from_relative(path)
_, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_path = _get_dvc_path(dvc_fs, subkey)
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
def ls( # pylint: disable=arguments-differ
self, path, detail=True, dvc_only=False, **kwargs
):
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
dvcignore = repo.dvcignore
ignore_subrepos = kwargs.get("ignore_subrepos", True)
key = self._get_key_from_relative(path)
repo, dvc_fs, subkey = self._get_subrepo_info(key)
names = set()
if dvc_fs:
with suppress(FileNotFoundError):
dvc_path = _get_dvc_path(dvc_fs, subkey)
for entry in dvc_fs.ls(dvc_path, detail=False):
names.add(dvc_fs.path.name(entry))
if not dvc_only and fs:
ignore_subrepos = kwargs.get("ignore_subrepos", True)
if not dvc_only:
fs = self.repo.fs
fs_path = self._from_key(key)
try:
for entry in dvcignore.ls(
for entry in repo.dvcignore.ls(
fs, fs_path, detail=False, ignore_subrepos=ignore_subrepos
):
names.add(fs.path.name(entry))
@@ -362,8 +272,14 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
paths = []
for name in names:
entry_path = self.path.join(path, name)
entry_key = key + (name,)
try:
info = self.info(entry_path, ignore_subrepos=ignore_subrepos)
info = self._info(
entry_key,
entry_path,
ignore_subrepos=ignore_subrepos,
check_ignored=False,
)
except FileNotFoundError:
continue
infos.append(info)
@@ -377,45 +293,52 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
def get_file( # pylint: disable=arguments-differ
self, rpath, lpath, callback=DEFAULT_CALLBACK, **kwargs
):
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(rpath)
if fs:
try:
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
return
except FileNotFoundError:
if not dvc_fs:
raise
key = self._get_key_from_relative(rpath)
fs_path = self._from_key(key)
fs = self.repo.fs
try:
fs.get_file(fs_path, lpath, callback=callback, **kwargs)
return
except FileNotFoundError:
_, dvc_fs, subkey = self._get_subrepo_info(key)
if not dvc_fs:
raise
dvc_path = _get_dvc_path(dvc_fs, subkey)
dvc_fs.get_file(dvc_path, lpath, callback=callback, **kwargs)
def info(self, path, **kwargs):
repo, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
dvcignore = repo.dvcignore
key = self._get_key_from_relative(path)
ignore_subrepos = kwargs.get("ignore_subrepos", True)
return self._info(key, path, ignore_subrepos=ignore_subrepos)
def _info(self, key, path, ignore_subrepos=True, check_ignored=True):
repo, dvc_fs, subkey = self._get_subrepo_info(key)
dvc_info = None
if dvc_fs:
try:
dvc_info = dvc_fs.info(dvc_path)
dvc_info = dvc_fs.fs.index.info(subkey)
dvc_path = _get_dvc_path(dvc_fs, subkey)
dvc_info["name"] = dvc_path
except FileNotFoundError:
pass
fs_info = None
if fs:
try:
fs_info = fs.info(fs_path)
if dvcignore.is_ignored(
fs, fs_path, ignore_subrepos=ignore_subrepos
):
fs_info = None
except (FileNotFoundError, NotADirectoryError):
if not dvc_info:
raise
fs = self.repo.fs
fs_path = self._from_key(key)
try:
fs_info = fs.info(fs_path)
if check_ignored and repo.dvcignore.is_ignored(
fs, fs_path, ignore_subrepos=ignore_subrepos
):
fs_info = None
except (FileNotFoundError, NotADirectoryError):
if not dvc_info:
raise
# NOTE: if some parent in fs_path turns out to be a file, it means
# that the whole repofs branch doesn't exist.
if fs and not fs_info and dvc_info:
if dvc_info and not fs_info:
for parent in fs.path.parents(fs_path):
try:
if fs.info(parent)["type"] != "directory":
@@ -431,14 +354,6 @@ class _DvcFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
info["name"] = path
return info
def checksum(self, path):
_, fs, fs_path, dvc_fs, dvc_path = self._get_fs_pair(path)
try:
return fs.checksum(fs_path)
except FileNotFoundError:
return dvc_fs.checksum(dvc_path)
class DvcFileSystem(FileSystem):
protocol = "local"
@@ -467,10 +382,6 @@ class DvcFileSystem(FileSystem):
def repo_url(self):
return self.fs.repo_url
@property
def config(self):
return self.fs.config
def from_os_path(self, path):
if os.path.isabs(path):
path = os.path.relpath(path, self.repo.root_dir)
+1
View File
@@ -15,6 +15,7 @@ class GitFileSystem(FileSystem): # pylint:disable=abstract-method
"""Proxies the repo file access methods to Git objects"""
protocol = "local"
PARAM_CHECKSUM = "md5"
def __init__(
self,
+1 -1
View File
@@ -1,5 +1,5 @@
from dvc.fs import GitFileSystem, Schemes
from dvc_data.db import get_odb
from dvc_data.hashfile.db import get_odb
def _get_odb(repo, settings, fs=None):
+145 -44
View File
@@ -1,10 +1,10 @@
import logging
import os
from collections import defaultdict
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type
from urllib.parse import urlparse
from funcy import collecting, project
from funcy import cached_property, collecting, project
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
from dvc import prompt
@@ -17,22 +17,25 @@ from dvc.exceptions import (
MergeError,
RemoteCacheRequiredError,
)
from dvc_data import Tree
from dvc_data import check as ocheck
from dvc_data import load as oload
from dvc_data.build import build
from dvc_data.checkout import checkout
from dvc_data.hashfile import Tree
from dvc_data.hashfile import check as ocheck
from dvc_data.hashfile import load as oload
from dvc_data.hashfile.build import build
from dvc_data.hashfile.checkout import checkout
from dvc_data.hashfile.hash_info import HashInfo
from dvc_data.hashfile.istextfile import istextfile
from dvc_data.hashfile.meta import Meta
from dvc_data.transfer import transfer as otransfer
from dvc_data.hashfile.transfer import transfer as otransfer
from dvc_objects.errors import ObjectFormatError
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
from .utils import relpath
from .utils.fs import path_isin
if TYPE_CHECKING:
from dvc_data.hashfile.obj import HashFile
from dvc_data.index import DataIndexKey
from dvc_objects.db import ObjectDB
logger = logging.getLogger(__name__)
@@ -80,9 +83,10 @@ def loadd_from(stage, d_list):
plot = d.pop(Output.PARAM_PLOT, False)
persist = d.pop(Output.PARAM_PERSIST, False)
checkpoint = d.pop(Output.PARAM_CHECKPOINT, False)
desc = d.pop(Output.PARAM_DESC, False)
live = d.pop(Output.PARAM_LIVE, False)
remote = d.pop(Output.PARAM_REMOTE, None)
annot = {field: d.pop(field, None) for field in ANNOTATION_FIELDS}
files = d.pop(Output.PARAM_FILES, None)
ret.append(
_get(
stage,
@@ -93,9 +97,10 @@ def loadd_from(stage, d_list):
plot=plot,
persist=persist,
checkpoint=checkpoint,
desc=desc,
live=live,
remote=remote,
**annot,
files=files,
)
)
return ret
@@ -187,6 +192,7 @@ def load_from_pipeline(stage, data, typ="outs"):
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
Output.PARAM_REMOTE,
*ANNOTATION_FIELDS,
],
)
@@ -240,6 +246,7 @@ class Output:
PARAM_PATH = "path"
PARAM_CACHE = "cache"
PARAM_CHECKPOINT = "checkpoint"
PARAM_FILES = "files"
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
PARAM_METRIC_XPATH = "xpath"
@@ -252,7 +259,6 @@ class Output:
PARAM_PLOT_TITLE = "title"
PARAM_PLOT_HEADER = "header"
PARAM_PERSIST = "persist"
PARAM_DESC = "desc"
PARAM_LIVE = "live"
PARAM_LIVE_SUMMARY = "summary"
PARAM_LIVE_HTML = "html"
@@ -284,11 +290,29 @@ class Output:
checkpoint=False,
live=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
meta=None,
remote=None,
repo=None,
fs_config=None,
files: List[Dict[str, Any]] = None,
):
self.annot = Annotation(
desc=desc, type=type, labels=labels or [], meta=meta or {}
)
self.repo = stage.repo if not repo and stage else repo
fs_cls, fs_config, fs_path = get_cloud_fs(self.repo, url=path)
meta = Meta.from_dict(info or {})
# NOTE: when version_aware is not passed into get_cloud_fs, it will be
# set based on whether or not path is versioned
fs_kwargs = {"version_aware": True} if meta.version_id else {}
if fs_config is not None:
fs_kwargs.update(**fs_config)
fs_cls, fs_config, fs_path = get_cloud_fs(
self.repo, url=path, **fs_kwargs
)
self.fs = fs_cls(**fs_config)
if (
@@ -320,21 +344,38 @@ class Output:
# By resolved path, which contains actual location,
# should be absolute and don't contain remote:// refs.
self.stage = stage
self.meta = Meta.from_dict(info)
self.hash_info = HashInfo.from_dict(info)
self.meta = meta
self._files = files
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.plot = False if self.IS_DEPENDENCY else plot
self.persist = persist
self.checkpoint = checkpoint
self.live = live
self.desc = desc
self.fs_path = self._parse_path(self.fs, fs_path)
self.obj = None
self.obj: Optional["HashFile"] = None
self.remote = remote
if self.fs.version_aware:
_, version_id = self.fs.path.coalesce_version(
self.def_path, self.meta.version_id
)
self.meta.version_id = version_id
if self.is_in_repo:
self.hash_name = "md5"
else:
self.hash_name = self.fs.PARAM_CHECKSUM
self.hash_info = HashInfo(
name=self.hash_name,
value=getattr(self.meta, self.hash_name, None),
)
if self.hash_info and self.hash_info.isdir:
self.meta.isdir = True
def _parse_path(self, fs, fs_path):
parsed = urlparse(self.def_path)
if (
@@ -379,6 +420,7 @@ class Output:
self.hash_info = HashInfo.from_dict({})
self.meta = Meta.from_dict({})
self.obj = None
self._files = None
@property
def protocol(self):
@@ -419,21 +461,27 @@ class Output:
)
def get_hash(self):
_, hash_info = self._get_hash_meta()
return hash_info
def _get_hash_meta(self):
if self.use_cache:
odb = self.odb
name = self.odb.fs.PARAM_CHECKSUM
else:
odb = self.repo.odb.local
name = self.fs.PARAM_CHECKSUM
_, _, obj = build(
_, meta, obj = build(
odb,
self.fs_path,
self.fs,
name,
self.hash_name,
ignore=self.dvcignore,
dry_run=not self.use_cache,
)
return obj.hash_info
return meta, obj.hash_info
def get_meta(self) -> Meta:
meta, _ = self._get_hash_meta()
return meta
@property
def is_dir_checksum(self):
@@ -452,6 +500,17 @@ class Output:
return self.fs.exists(self.fs_path)
@cached_property
def index_key(self) -> Tuple[str, "DataIndexKey"]:
if self.is_in_repo:
workspace = "repo"
key = self.repo.fs.path.relparts(self.fs_path, self.repo.root_dir)
else:
workspace = self.fs.protocol
no_drive = self.fs.path.flavour.splitdrive(self.fs_path)[1]
key = self.fs.path.parts(no_drive)[1:]
return workspace, key
def changed_checksum(self):
return self.hash_info != self.get_hash()
@@ -469,6 +528,11 @@ class Output:
except (FileNotFoundError, ObjectFormatError):
return True
def changed_meta(self) -> bool:
if self.fs.version_aware and self.meta.version_id:
return self.meta.version_id == self.get_meta().version_id
return False
def workspace_status(self):
if not self.exists:
return {str(self): "deleted"}
@@ -551,11 +615,12 @@ class Output:
self.repo.odb.local,
self.fs_path,
self.fs,
self.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
dry_run=True,
)
self.hash_info = obj.hash_info
self._files = None
if not self.IS_DEPENDENCY:
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
@@ -568,18 +633,19 @@ class Output:
self.odb,
self.fs_path,
self.fs,
self.odb.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
)
self.hash_info = self.obj.hash_info
self._files = None
def set_exec(self):
if self.isfile() and self.meta.isexec:
self.odb.set_exec(self.fs_path)
def _checkout(self, *args, **kwargs):
from dvc_data.checkout import CheckoutError as _CheckoutError
from dvc_data.checkout import LinkError, PromptError
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
from dvc_data.hashfile.checkout import LinkError, PromptError
kwargs.setdefault("ignore", self.dvcignore)
try:
@@ -610,7 +676,7 @@ class Output:
self.odb,
filter_info or self.fs_path,
self.fs,
self.odb.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
)
otransfer(
@@ -639,7 +705,7 @@ class Output:
self.odb,
self.fs_path,
self.fs,
self.odb.fs.PARAM_CHECKSUM,
self.hash_name,
ignore=self.dvcignore,
)
save_obj = save_obj.filter(prefix)
@@ -653,8 +719,10 @@ class Output:
)
return checkout_obj
def dumpd(self):
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
def dumpd(self, **kwargs):
meta = self.meta.to_dict()
meta.pop("isdir", None)
ret = {**self.hash_info.to_dict(), **meta}
if self.is_in_repo:
path = self.fs.path.as_posix(
@@ -668,9 +736,7 @@ class Output:
if self.IS_DEPENDENCY:
return ret
if self.desc:
ret[self.PARAM_DESC] = self.desc
ret.update(self.annot.to_dict())
if not self.use_cache:
ret[self.PARAM_CACHE] = self.use_cache
@@ -699,6 +765,18 @@ class Output:
if self.remote:
ret[self.PARAM_REMOTE] = self.remote
if (
self.use_cache
and self.is_in_repo
and self.hash_info.isdir
and (kwargs.get("with_files") or self._files is not None)
):
if self.obj:
obj = self.obj
else:
obj = self.get_obj()
ret[self.PARAM_FILES] = obj.as_list(with_meta=True)
return ret
def verify_metric(self):
@@ -731,14 +809,22 @@ class Output:
) as cb:
self.fs.get(self.fs_path, to.fs_path, batch_size=jobs, callback=cb)
def get_obj(self, filter_info=None, **kwargs):
def get_obj(
self, filter_info: Optional[str] = None, **kwargs
) -> Optional["HashFile"]:
obj: Optional["HashFile"] = None
if self.obj:
obj = self.obj
elif self.hash_info:
try:
obj = oload(self.odb, self.hash_info)
except (FileNotFoundError, ObjectFormatError):
return None
if self._files:
tree = Tree.from_list(self._files, hash_name=self.hash_name)
tree.digest()
obj = tree
else:
try:
obj = oload(self.odb, self.hash_info)
except (FileNotFoundError, ObjectFormatError):
return None
else:
return None
@@ -858,6 +944,7 @@ class Output:
)
self.hash_info = obj.hash_info
self._files = None
return obj
def get_files_number(self, filter_info=None):
@@ -928,6 +1015,7 @@ class Output:
obj = self.get_obj()
if filter_info and filter_info != self.fs_path:
assert obj
prefix = self.fs.path.parts(
self.fs.path.relpath(filter_info, self.fs_path)
)
@@ -1020,7 +1108,7 @@ class Output:
other = out.dumpd()
ignored = [
self.fs.PARAM_CHECKSUM,
self.hash_name,
Meta.PARAM_SIZE,
Meta.PARAM_NFILES,
]
@@ -1040,8 +1128,8 @@ class Output:
)
def merge(self, ancestor, other):
from dvc_data.objects.tree import MergeError as TreeMergeError
from dvc_data.objects.tree import du, merge
from dvc_data.hashfile.tree import MergeError as TreeMergeError
from dvc_data.hashfile.tree import du, merge
assert other
@@ -1064,6 +1152,7 @@ class Output:
self.odb.add(merged.path, merged.fs, merged.oid)
self.hash_info = merged.hash_info
self._files = None
self.meta = Meta(
size=du(self.odb, merged),
nfiles=len(merged),
@@ -1086,21 +1175,33 @@ class Output:
return bool(self.plot) or bool(self.live)
META_SCHEMA = {
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Meta.PARAM_ISEXEC: bool,
Meta.PARAM_VERSION_ID: str,
}
ARTIFACT_SCHEMA = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required(Output.PARAM_PATH): str,
Output.PARAM_PLOT: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Meta.PARAM_ISEXEC: bool,
}
DIR_FILES_SCHEMA: Dict[str, Any] = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required(Tree.PARAM_RELPATH): str,
}
SCHEMA = {
**ARTIFACT_SCHEMA,
**ANNOTATION_SCHEMA,
Output.PARAM_CACHE: bool,
Output.PARAM_METRIC: Output.METRIC_SCHEMA,
Output.PARAM_DESC: str,
Output.PARAM_REMOTE: str,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
+1 -1
View File
@@ -159,7 +159,7 @@ class Value(Node):
PRIMITIVES = (int, float, str, bytes, bool)
class Container(Node, ABC):
class Container(Node, ABC): # noqa: B024
meta: Meta
data: Union[list, dict]
_key_transform = staticmethod(identity)
+3 -2
View File
@@ -264,6 +264,7 @@ def create_stages(
outs=[out],
external=external,
)
if kwargs.get("desc"):
stage.outs[0].desc = kwargs["desc"]
out_obj = stage.outs[0]
out_obj.annot.update(**kwargs)
yield stage
+30 -6
View File
@@ -1,13 +1,16 @@
import os
import posixpath
from collections import defaultdict
from itertools import chain
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
List,
Optional,
Tuple,
TypedDict,
cast,
)
@@ -74,8 +77,8 @@ def _granular_diff(
new_obj: Optional["HashFile"],
cache: "HashFileDB",
) -> Dict[str, List[str]]:
from dvc_data.diff import ROOT
from dvc_data.diff import diff as odiff
from dvc_data.hashfile.diff import ROOT
from dvc_data.hashfile.diff import diff as odiff
def path_join(root: str, *paths: str, isdir: bool = False) -> str:
if not isdir and paths == ROOT:
@@ -102,7 +105,7 @@ def _get_obj_items(root: str, obj: Optional["HashFile"]) -> List[str]:
if not obj:
return []
from dvc_data.objects.tree import Tree
from dvc_data.hashfile.tree import Tree
sep = os.path.sep
if isinstance(obj, Tree):
@@ -153,6 +156,8 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
empty_repo = False
staged, unstaged, untracked = scm.status(untracked_files=untracked_files)
if os.name == "nt":
untracked = [posixpath_to_os_path(path) for path in untracked]
# NOTE: order is important here.
return GitInfo(
staged=staged,
@@ -164,7 +169,7 @@ def _git_info(scm: "Base", untracked_files: str = "all") -> GitInfo:
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
from dvc_data.build import build
from dvc_data.hashfile.build import build
unstaged_diff = defaultdict(list)
for out in repo.index.outs:
@@ -210,7 +215,9 @@ def _diff_head_to_index(
repo: "Repo", head: str = "HEAD", **kwargs: Any
) -> Dict[str, List[str]]:
# we need to store objects from index and the HEAD to diff later
objs: Dict[str, Dict[str, "HashFile"]] = defaultdict(dict)
objs: Dict[str, Dict[str, Tuple["HashFile", "HashInfo"]]]
objs = defaultdict(dict)
staged_diff = defaultdict(list)
for rev in repo.brancher(revs=[head]):
for out in repo.index.outs:
@@ -250,7 +257,9 @@ class Status(TypedDict):
git: GitInfo
def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]):
def _transform_git_paths_to_dvc(
repo: "Repo", files: Iterable[str]
) -> List[str]:
"""Transform files rel. to Git root to DVC root, and drop outside files."""
rel = repo.fs.path.relpath(repo.root_dir, repo.scm.root_dir).rstrip("/")
@@ -298,3 +307,18 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
unchanged=list(unchanged),
git=git_info,
)
def ls(
repo: "Repo",
targets: List[Optional[str]] = None,
recursive: bool = False,
) -> Iterator[Dict[str, Any]]:
targets = targets or [None]
pairs = chain.from_iterable(
repo.stage.collect_granular(target, recursive=recursive)
for target in targets
)
for stage, filter_info in pairs:
for out in stage.filter_outs(filter_info):
yield {"path": str(out), **out.annot.to_dict()}
+1 -1
View File
@@ -115,7 +115,7 @@ def _paths_checksums(repo, targets):
def _output_paths(repo, targets):
from dvc.fs import LocalFileSystem
from dvc_data.build import build
from dvc_data.hashfile.build import build
on_working_fs = isinstance(repo.fs, LocalFileSystem)
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
class BaseExecutorManager(ABC, Mapping):
class BaseExecutorManager(ABC, Mapping): # noqa: B024
"""Manages executors for a collection of experiments to be run."""
EXECUTOR_CLS: Type = BaseExecutor
+34 -8
View File
@@ -2,6 +2,7 @@ import logging
from typing import Dict, Iterable, Optional
from dvc.dependency.param import ParamsDependency
from dvc.exceptions import InvalidArgumentError
from dvc.repo import locked
from dvc.ui import ui
from dvc.utils.cli_parse import to_path_overrides
@@ -27,6 +28,8 @@ def run(
Returns a dict mapping new experiment SHAs to the results
of `repro` for that experiment.
"""
from dvc.utils.hydra import to_hydra_overrides
if run_all:
entries = list(repo.experiments.celery_queue.iter_queued())
return repo.experiments.reproduce_celery(entries, jobs=jobs)
@@ -42,19 +45,42 @@ def run(
# Force `_update_params` even if `--set-param` was not used
path_overrides[hydra_output_file] = []
if queue:
if not kwargs.get("checkpoint_resume", None):
kwargs["reset"] = True
hydra_sweep = any(
x.is_sweep_override()
for param_file in path_overrides
for x in to_hydra_overrides(path_overrides[param_file])
)
if hydra_sweep and not queue:
raise InvalidArgumentError(
"Sweep overrides can't be used without `--queue`"
)
if not queue:
return repo.experiments.reproduce_one(
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
)
if hydra_sweep:
from dvc.utils.hydra import get_hydra_sweeps
sweeps = get_hydra_sweeps(path_overrides)
else:
sweeps = [path_overrides]
if not kwargs.get("checkpoint_resume", None):
kwargs["reset"] = True
for sweep_overrides in sweeps:
queue_entry = repo.experiments.queue_one(
repo.experiments.celery_queue,
targets=targets,
params=path_overrides,
params=sweep_overrides,
**kwargs,
)
if sweep_overrides:
ui.write(f"Queueing with overrides '{sweep_overrides}'.")
name = queue_entry.name or queue_entry.stash_rev[:7]
ui.write(f"Queued experiment '{name}' for future execution.")
return {}
return repo.experiments.reproduce_one(
targets=targets, params=path_overrides, tmp_dir=tmp_dir, **kwargs
)
return {}
+37
View File
@@ -1,6 +1,8 @@
import logging
from contextlib import suppress
from typing import TYPE_CHECKING, Optional
from dvc.config import NoRemoteError
from dvc.exceptions import DownloadError, FileTransferError
from dvc.fs import Schemes
@@ -12,6 +14,36 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _fetch_worktree(repo, remote):
from dvc_data.index import save
index = repo.index.data["repo"]
for key, entry in index.iteritems():
entry.fs = remote.fs
entry.path = remote.fs.path.join(
remote.path,
*key,
)
save(index)
for stage in repo.index.stages:
for out in stage.outs:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
workspace, key = out.index_key
entry = repo.index.data[workspace][key]
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage)
return len(index)
@locked
def fetch(
self,
@@ -42,6 +74,11 @@ def fetch(
if isinstance(targets, str):
targets = [targets]
with suppress(NoRemoteError):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _fetch_worktree(self, _remote)
used = self.used_objs(
targets,
all_branches=all_branches,
+2 -2
View File
@@ -60,8 +60,8 @@ def gc(
from contextlib import ExitStack
from dvc.repo import Repo
from dvc_data.db import get_index
from dvc_data.gc import gc as ogc
from dvc_data.hashfile.db import get_index
from dvc_data.hashfile.gc import gc as ogc
if not repos:
repos = []
+7 -3
View File
@@ -22,7 +22,11 @@ def imp_url(
remote=None,
to_remote=False,
desc=None,
type=None, # pylint: disable=redefined-builtin
labels=None,
meta=None,
jobs=None,
fs_config=None,
):
from dvc.dvcfile import Dvcfile
from dvc.stage import Stage, create_stage, restore_fields
@@ -58,12 +62,12 @@ def imp_url(
deps=[url],
outs=[out],
erepo=erepo,
fs_config=fs_config,
)
restore_fields(stage)
if desc:
stage.outs[0].desc = desc
out_obj = stage.outs[0]
out_obj.annot.update(desc=desc, type=type, labels=labels, meta=meta)
dvcfile = Dvcfile(self, stage.path)
dvcfile.remove()
+2 -9
View File
@@ -183,15 +183,7 @@ class Index:
if not out.use_cache:
continue
if out.is_in_repo:
workspace = "repo"
key = self.repo.fs.path.relparts(
out.fs_path, self.repo.root_dir
)
else:
workspace = out.fs.protocol
no_drive = out.fs.path.flavour.splitdrive(out.fs_path)[1]
key = out.fs.path.parts(no_drive)[1:]
workspace, key = out.index_key
try:
remote = self.repo.cloud.get_remote_odb(out.remote)
@@ -205,6 +197,7 @@ class Index:
obj=out.obj,
hash_info=out.hash_info,
odb=out.odb,
cache=out.odb,
remote=remote,
)
+50
View File
@@ -9,6 +9,52 @@ if TYPE_CHECKING:
from dvc_objects.db.base import ObjectDB
def _push_worktree(repo, remote):
from dvc_data.hashfile.tree import tree_from_index
from dvc_data.index import build, checkout, collect
index = repo.index.data["repo"]
checkout(index, remote.path, remote.fs, force=True)
build(index, remote.path, remote.fs)
if any(out.isdir() for out in repo.index.outs):
collect(index, remote.path, remote.fs)
for stage in repo.index.stages:
for out in stage.outs:
if not out.use_cache:
continue
if not out.is_in_repo:
continue
workspace, key = out.index_key
index = repo.index.data[workspace]
entry = index[key]
if out.isdir():
old_tree = out.get_obj()
entry.hash_info = old_tree.hash_info
entry.meta = out.meta
for subkey, entry in index.iteritems(key):
if entry.meta.isdir:
continue
fs_path = repo.fs.path.join(repo.root_dir, *subkey)
_, hash_info = old_tree.get(
repo.fs.path.relparts(fs_path, out.fs_path)
)
entry.hash_info = hash_info
tree_meta, new_tree = tree_from_index(index, key)
new_tree.digest()
out.obj = new_tree
out.hash_info = new_tree.hash_info
out.meta = tree_meta
else:
out.hash_info = entry.hash_info
out.meta = entry.meta
stage.dvcfile.dump(stage, with_files=True)
return len(index)
@locked
def push(
self,
@@ -26,6 +72,10 @@ def push(
odb: Optional["ObjectDB"] = None,
include_imports=False,
):
_remote = self.cloud.get_remote(name=remote)
if _remote.worktree:
return _push_worktree(self, _remote)
used_run_cache = (
self.stage_cache.push(remote, odb=odb) if run_cache else []
)
+1 -1
View File
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
@locked
def remove(self: "Repo", target: str, outs: bool = False):
try:
stages = self.stage.from_target(target)
stages = self.stage.from_target(target, accept_group=False)
except (StageNotFound, StageFileDoesNotExistError) as e:
# If the user specified a tracked file as a target instead of a stage,
# e.g. `data.csv` instead of `data.csv.dvc`,
-2
View File
@@ -102,7 +102,6 @@ def reproduce(
from .graph import get_pipeline, get_pipelines
glob = kwargs.pop("glob", False)
accept_group = not glob
if isinstance(targets, str):
targets = [targets]
@@ -137,7 +136,6 @@ def reproduce(
self.stage.collect(
target,
recursive=recursive,
accept_group=accept_group,
glob=glob,
)
)
+11 -22
View File
@@ -82,7 +82,6 @@ def _collect_specific_target(
target: str,
with_deps: bool,
recursive: bool,
accept_group: bool,
) -> Tuple[StageIter, "OptStr", "OptStr"]:
from dvc.dvcfile import is_valid_filename
@@ -98,13 +97,11 @@ def _collect_specific_target(
msg = "Checking if stage '%s' is in '%s'"
logger.debug(msg, target, PIPELINE_FILE)
if not (recursive and loader.fs.isdir(target)):
stages = _maybe_collect_from_dvc_yaml(
loader, target, with_deps, accept_group=accept_group
)
stages = _maybe_collect_from_dvc_yaml(loader, target, with_deps)
if stages:
return stages, file, name
elif not with_deps and is_valid_filename(file):
stages = loader.load_all(file, name, accept_group=accept_group)
stages = loader.load_all(file, name)
return stages, file, name
return [], file, name
@@ -209,7 +206,7 @@ class StageLoad:
return stage
def from_target(
self, target: str, accept_group: bool = False, glob: bool = False
self, target: str, accept_group: bool = True, glob: bool = False
) -> StageList:
"""
Returns a list of stage from the provided target.
@@ -250,15 +247,12 @@ class StageLoad:
self,
stages: "StageLoader",
name: str = None,
accept_group: bool = False,
accept_group: bool = True,
glob: bool = False,
) -> Iterable[str]:
assert not (accept_group and glob)
if not name:
return stages.keys()
if accept_group and stages.is_foreach_generated(name):
return self._get_group_keys(stages, name)
if glob:
@@ -269,7 +263,7 @@ class StageLoad:
self,
path: str = None,
name: str = None,
accept_group: bool = False,
accept_group: bool = True,
glob: bool = False,
) -> StageList:
"""Load a list of stages from a file.
@@ -338,7 +332,6 @@ class StageLoad:
with_deps: bool = False,
recursive: bool = False,
graph: "DiGraph" = None,
accept_group: bool = False,
glob: bool = False,
) -> StageIter:
"""Collect list of stages from the provided target.
@@ -347,15 +340,14 @@ class StageLoad:
target: if not provided, all of the stages in the graph are
returned.
Target can be:
- a stage name in the `dvc.yaml` file.
- a foreach group name or a stage name in the `dvc.yaml` file.
- a generated stage name from a foreach group.
- a path to `dvc.yaml` or `.dvc` file.
- in case of a stage to a dvc.yaml file in a different
directory than current working directory, it can be a path
to dvc.yaml file, followed by a colon `:`, followed by stage
name (eg: `../dvc.yaml:build`).
- in case of `recursive`, it can be a path to a directory.
- in case of `accept_group`, it can be a group name of
`foreach` generated stage.
- in case of `glob`, it can be a wildcard pattern to match
stages. Example: `build*` for stages in `dvc.yaml` file, or
`../dvc.yaml:build*` for stages in dvc.yaml in a different
@@ -367,8 +359,6 @@ class StageLoad:
recursive: if true and if `target` is a directory, all of the
stages inside that directory is returned.
graph: graph to use. Defaults to `repo.graph`.
accept_group: if true, all of the `foreach` generated stages of
the specified target is returned.
glob: Use `target` as a pattern to match stages in a file.
"""
if not target:
@@ -380,7 +370,7 @@ class StageLoad:
path = self.fs.path.abspath(target)
return collect_inside_path(path, graph or self.graph)
stages = self.from_target(target, accept_group=accept_group, glob=glob)
stages = self.from_target(target, glob=glob)
if not with_deps:
return stages
@@ -392,14 +382,14 @@ class StageLoad:
with_deps: bool = False,
recursive: bool = False,
graph: "DiGraph" = None,
accept_group: bool = False,
) -> List[StageInfo]:
"""Collects a list of (stage, filter_info) from the given target.
Priority is in the order of following in case of ambiguity:
- .dvc file or .yaml file
- dir if recursive and directory exists
- stage_name
- foreach_group_name or stage_name
- generated stage name from a foreach group
- output file
Args:
@@ -418,7 +408,7 @@ class StageLoad:
target = as_posix(target)
stages, file, _ = _collect_specific_target(
self, target, with_deps, recursive, accept_group
self, target, with_deps, recursive
)
if not stages:
if not (recursive and self.fs.isdir(target)):
@@ -440,7 +430,6 @@ class StageLoad:
with_deps,
recursive,
graph,
accept_group=accept_group,
)
except StageFileDoesNotExistError as exc:
# collect() might try to use `target` as a stage name
+14 -7
View File
@@ -3,11 +3,11 @@ from collections.abc import Mapping
from voluptuous import Any, Optional, Required, Schema
from dvc import dependency, output
from dvc.output import CHECKSUMS_SCHEMA, Output
from dvc.annotations import ANNOTATION_SCHEMA
from dvc.output import CHECKSUMS_SCHEMA, DIR_FILES_SCHEMA, META_SCHEMA, Output
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
from dvc.stage.params import StageParams
from dvc_data.hashfile.meta import Meta
STAGES = "stages"
SINGLE_STAGE_SCHEMA = {
@@ -25,10 +25,9 @@ SINGLE_STAGE_SCHEMA = {
DATA_SCHEMA = {
**CHECKSUMS_SCHEMA,
**META_SCHEMA,
Required("path"): str,
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Meta.PARAM_ISEXEC: bool,
Output.PARAM_FILES: [DIR_FILES_SCHEMA],
}
LOCK_FILE_STAGE_SCHEMA = {
Required(StageParams.PARAM_CMD): Any(str, list),
@@ -47,16 +46,15 @@ LOCKFILE_V2_SCHEMA = {
OUT_PSTAGE_DETAILED_SCHEMA = {
str: {
**ANNOTATION_SCHEMA, # type: ignore
Output.PARAM_CACHE: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
Output.PARAM_DESC: str,
Output.PARAM_REMOTE: str,
}
}
PLOTS = "plots"
PLOTS_SCHEMA = dict
PLOT_PROPS = {
Output.PARAM_PLOT_TEMPLATE: str,
Output.PARAM_PLOT_X: str,
@@ -112,6 +110,15 @@ def either_or(primary, fallback, fallback_includes=None):
return validator
PLOT_DEFINITION = {
Output.PARAM_PLOT_X: str,
Output.PARAM_PLOT_Y: Any(str, [str], {str: Any(str, [str])}),
Output.PARAM_PLOT_X_LABEL: str,
Output.PARAM_PLOT_Y_LABEL: str,
Output.PARAM_PLOT_TITLE: str,
Output.PARAM_PLOT_TEMPLATE: str,
}
PLOTS_SCHEMA = {str: Any(PLOT_DEFINITION, None)}
FOREACH_IN = {
Required(FOREACH_KWD): Any(dict, list, str),
Required(DO_KWD): STAGE_DEFINITION,
+6 -9
View File
@@ -88,7 +88,7 @@ def create_stage(cls, repo, path, external=False, **kwargs):
if not external:
check_no_externals(stage)
fill_stage_dependencies(
stage, **project(kwargs, ["deps", "erepo", "params"])
stage, **project(kwargs, ["deps", "erepo", "params", "fs_config"])
)
check_circular_dependency(stage)
check_duplicated_arguments(stage)
@@ -110,16 +110,13 @@ def restore_fields(stage):
# will be used to restore comments later
# noqa, pylint: disable=protected-access
stage._stage_text = old._stage_text
stage.meta = old.meta
stage.desc = old.desc
old_fields = {out.def_path: (out.desc, out.remote) for out in old.outs}
old_fields = {out.def_path: (out.annot, out.remote) for out in old.outs}
for out in stage.outs:
out_fields = old_fields.get(out.def_path, None)
if out_fields:
out.desc, out.remote = out_fields
if out_fields := old_fields.get(out.def_path, None):
out.annot, out.remote = out_fields
class Stage(params.StageParams):
@@ -464,8 +461,8 @@ class Stage(params.StageParams):
def reload(self):
return self.dvcfile.stage
def dumpd(self):
return get_dump(self)
def dumpd(self, **kwargs):
return get_dump(self, **kwargs)
def compute_md5(self):
# `dvc add`ed files don't need stage md5
+1 -1
View File
@@ -9,7 +9,7 @@ from funcy import cached_property, first
from dvc import fs
from dvc.exceptions import DvcException
from dvc.utils import dict_sha256, relpath
from dvc_data.transfer import _log_exceptions
from dvc_data.hashfile.transfer import _log_exceptions
if TYPE_CHECKING:
from dvc_objects.db import ObjectDB
+2 -1
View File
@@ -69,7 +69,8 @@ class StageLoader(Mapping):
info = info.copy()
info.pop("path", None)
item.meta = Meta.from_dict(info)
item.hash_info = HashInfo.from_dict(info)
hash_value = getattr(item.meta, item.hash_name, None)
item.hash_info = HashInfo(item.hash_name, hash_value)
@classmethod
def load_stage(cls, dvcfile, name, stage_data, lock_data=None):
+10 -7
View File
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, List, no_type_check
from funcy import post_processing
from dvc.dependency import ParamsDependency
from dvc.output import Output
from dvc.output import Annotation, Output
from dvc.utils.collections import apply_diff
from dvc.utils.serialize import parse_yaml_for_update
@@ -27,7 +27,7 @@ PARAM_METRIC = Output.PARAM_METRIC
PARAM_PLOT = Output.PARAM_PLOT
PARAM_PERSIST = Output.PARAM_PERSIST
PARAM_CHECKPOINT = Output.PARAM_CHECKPOINT
PARAM_DESC = Output.PARAM_DESC
PARAM_DESC = Annotation.PARAM_DESC
PARAM_REMOTE = Output.PARAM_REMOTE
DEFAULT_PARAMS_FILE = ParamsDependency.DEFAULT_PARAMS_FILE
@@ -38,8 +38,9 @@ sort_by_path = partial(sorted, key=attrgetter("def_path"))
@post_processing(OrderedDict)
def _get_flags(out):
if out.desc:
yield PARAM_DESC, out.desc
annot = out.annot.to_dict()
yield from annot.items()
if not out.use_cache:
yield PARAM_CACHE, False
if out.checkpoint:
@@ -155,10 +156,12 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
assert stage.cmd
def _dumpd(item):
meta_d = item.meta.to_dict()
meta_d.pop("isdir", None)
ret = [
(item.PARAM_PATH, item.def_path),
*item.hash_info.to_dict().items(),
*item.meta.to_dict().items(),
*meta_d.items(),
]
return OrderedDict(ret)
@@ -185,8 +188,8 @@ def to_lockfile(stage: "PipelineStage") -> dict:
return {stage.name: to_single_stage_lockfile(stage)}
def to_single_stage_file(stage: "Stage"):
state = stage.dumpd()
def to_single_stage_file(stage: "Stage", **kwargs):
state = stage.dumpd(**kwargs)
# When we load a stage we parse yaml with a fast parser, which strips
# off all the comments and formatting. To retain those on update we do
+11 -6
View File
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Union
from funcy import concat, first, lsplit, rpartial, without
from dvc.annotations import ANNOTATION_FIELDS
from dvc.exceptions import InvalidArgumentError
from dvc_data.hashfile.meta import Meta
@@ -107,12 +108,16 @@ def _load_live_output(
return outs
def fill_stage_dependencies(stage, deps=None, erepo=None, params=None):
def fill_stage_dependencies(
stage, deps=None, erepo=None, params=None, fs_config=None
):
from dvc.dependency import loads_from, loads_params
assert not stage.deps
stage.deps = []
stage.deps += loads_from(stage, deps or [], erepo=erepo)
stage.deps += loads_from(
stage, deps or [], erepo=erepo, fs_config=fs_config
)
stage.deps += loads_params(stage, params or [])
@@ -194,9 +199,9 @@ def compute_md5(stage):
return dict_md5(
d,
exclude=[
*ANNOTATION_FIELDS,
stage.PARAM_LOCKED, # backward compatibility
stage.PARAM_FROZEN,
Output.PARAM_DESC,
Output.PARAM_METRIC,
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
@@ -221,7 +226,7 @@ def resolve_paths(fs, path, wdir=None):
return path, wdir
def get_dump(stage):
def get_dump(stage, **kwargs):
return {
key: value
for key, value in {
@@ -230,8 +235,8 @@ def get_dump(stage):
stage.PARAM_CMD: stage.cmd,
stage.PARAM_WDIR: resolve_wdir(stage.wdir, stage.path),
stage.PARAM_FROZEN: stage.frozen,
stage.PARAM_DEPS: [d.dumpd() for d in stage.deps],
stage.PARAM_OUTS: [o.dumpd() for o in stage.outs],
stage.PARAM_DEPS: [d.dumpd(**kwargs) for d in stage.deps],
stage.PARAM_OUTS: [o.dumpd(**kwargs) for o in stage.outs],
stage.PARAM_ALWAYS_CHANGED: stage.always_changed,
stage.PARAM_META: stage.meta,
}.items()
+35 -6
View File
@@ -1,11 +1,13 @@
from collections import defaultdict
from pathlib import Path
from typing import TYPE_CHECKING, List
from hydra import compose, initialize_config_dir
from hydra._internal.config_loader_impl import ConfigLoaderImpl
from hydra._internal.core_plugins.basic_sweeper import BasicSweeper
from hydra.core.override_parser.overrides_parser import OverridesParser
from hydra.core.override_parser.types import ValueType
from hydra.errors import ConfigCompositionException, OverrideParseException
from hydra.types import RunMode
from omegaconf import OmegaConf
from dvc.exceptions import InvalidArgumentError
@@ -59,11 +61,7 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
modify_data = MODIFIERS[suffix]
with modify_data(path) as original_data:
try:
parser = OverridesParser.create()
parsed = parser.parse_overrides(overrides=overrides)
ConfigLoaderImpl.validate_sweep_overrides_legal(
parsed, run_mode=RunMode.RUN, from_shell=True
)
parsed = to_hydra_overrides(overrides)
new_data = OmegaConf.create(
to_omegaconf(original_data),
@@ -78,3 +76,34 @@ def apply_overrides(path: "StrPath", overrides: List[str]) -> None:
merge_dicts(original_data, new_data)
remove_missing_keys(original_data, new_data)
def to_hydra_overrides(path_overrides):
parser = OverridesParser.create()
return parser.parse_overrides(overrides=path_overrides)
def get_hydra_sweeps(path_overrides):
merged_overrides = []
for path, overrides in path_overrides.items():
merged_overrides.extend(
[f"{path}___{override}" for override in overrides]
)
hydra_overrides = to_hydra_overrides(merged_overrides)
for hydra_override in hydra_overrides:
if hydra_override.value_type == ValueType.GLOB_CHOICE_SWEEP:
raise InvalidArgumentError(
f"Glob override '{hydra_override.input_line}' "
"is not supported."
)
splits = BasicSweeper.split_arguments(hydra_overrides, None)[0]
sweeps = []
for split in splits:
sweep_overrides = defaultdict(list)
for merged_override in split:
path, override = merged_override.split("___")
sweep_overrides[path].append(override)
sweeps.append(dict(sweep_overrides))
return sweeps
+11 -9
View File
@@ -62,11 +62,11 @@ install_requires =
rich>=10.13.0
pyparsing>=2.4.7
typing-extensions>=3.7.4
scmrepo==0.0.25
dvc-render==0.0.9
scmrepo==0.1.1
dvc-render==0.0.10
dvc-task==0.1.2
dvclive>=0.10.0
dvc-data==0.4.0
dvc-data==0.8.0
dvc-http==2.19.1
hydra-core>=1.1.0
@@ -86,12 +86,13 @@ dev =
%(tests)s
dvc-data[cli]
# Extra dependencies for remote integrations
azure = dvc-azure==2.19.0
azure = dvc-azure==2.20.2
gdrive = dvc-gdrive==2.19.0
gs = dvc-gs==2.19.1
hdfs = dvc-hdfs==2.19.0; python_version < '3.11'
hdfs =
dvc-hdfs==2.19.0; python_version < '3.11'
oss = dvc-oss==2.19.0
s3 = dvc-s3==2.19.0
s3 = dvc-s3==2.20.0
ssh = dvc-ssh==2.19.0
# gssapi should not be included in all_remotes, because it doesn't have wheels
# for Linux, so it will fail to compile if user doesn't have all the
@@ -106,7 +107,7 @@ tests =
%(terraform)s
dvc-ssh==2.19.0
# Test requirements
pytest==7.1.2
pytest==7.1.3
pytest-cov==3.0.0
pytest-xdist==2.5.0
pytest-mock==3.8.2
@@ -115,11 +116,10 @@ tests =
# https://github.com/docker/docker-py/issues/2902
pytest-docker==0.11.0; python_version < '3.10' or sys_platform != 'win32'
flaky==3.7.0
mock==4.0.3
pytest-timeout==2.1.0
filelock==3.8.0
# pylint requirements
pylint==2.15.0
pylint==2.15.2
# we use this to suppress pytest-related false positives in our tests.
pylint-pytest==1.1.2
# we use this to suppress some messages in tests, eg: foo/bar naming,
@@ -145,6 +145,8 @@ exclude =
[options.entry_points]
console_scripts =
dvc = dvc.cli:main
fsspec.specs =
dvc = dvc.api:DvcFileSystem
[flake8]
ignore=
+1 -1
View File
@@ -3,7 +3,7 @@ import pytest
from dvc.exceptions import DownloadError, UploadError
from dvc.fs import LocalFileSystem
from dvc.utils.fs import remove
from dvc_data.db import get_index
from dvc_data.hashfile.db import get_index
@pytest.fixture
+56
View File
@@ -2,6 +2,8 @@ import sys
import pytest
from dvc.exceptions import InvalidArgumentError
from ..utils.test_hydra import hydra_setup
@@ -85,3 +87,57 @@ def test_hydra_compose_and_dump(
"goo": {"bag": 3.0},
"lorem": False,
}
@pytest.mark.parametrize(
"hydra_enabled,overrides,expected",
[
(
pytest.param(
True,
marks=pytest.mark.xfail(
sys.version_info >= (3, 11), reason="unsupported on 3.11"
),
),
["db=mysql,postgresql"],
[
{"params.yaml": ["db=mysql"]},
{"params.yaml": ["db=postgresql"]},
],
),
(
False,
["foo=bar,baz"],
[{"params.yaml": ["foo=bar"]}, {"params.yaml": ["foo=baz"]}],
),
],
)
def test_hydra_sweep(
tmp_dir, params_repo, dvc, mocker, hydra_enabled, overrides, expected
):
patched = mocker.patch.object(dvc.experiments, "queue_one")
if hydra_enabled:
hydra_setup(
tmp_dir,
config_dir="conf",
config_name="config",
)
with dvc.config.edit() as conf:
conf["hydra"]["enabled"] = True
dvc.experiments.run(params=overrides, queue=True)
assert patched.call_count == len(expected)
for e in expected:
patched.assert_any_call(
mocker.ANY,
params=e,
reset=True,
targets=None,
)
def test_hydra_sweep_requires_queue(params_repo, dvc):
with pytest.raises(InvalidArgumentError):
dvc.experiments.run(params=["db=mysql,postgresql"])
+32 -3
View File
@@ -12,6 +12,7 @@ import pytest
import dvc as dvc_module
import dvc_data
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dvcfile import DVC_FILE_SUFFIX
from dvc.exceptions import (
@@ -103,7 +104,7 @@ def test_add_unsupported_file(dvc):
def test_add_directory(tmp_dir, dvc):
from dvc_data import load
from dvc_data.hashfile import load
(stage,) = tmp_dir.dvc_gen({"dir": {"file": "file"}})
@@ -457,7 +458,7 @@ class TestAddCommit(TestDvc):
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
tmp_dir.gen({"data/data": "foo"})
counter = mocker.spy(dvc_data.build, "_build_tree")
counter = mocker.spy(dvc_data.hashfile.build, "_build_tree")
ret = main(["add", "data"])
assert ret == 0
assert counter.mock.call_count == 3
@@ -881,7 +882,7 @@ def test_add_with_cache_link_error(tmp_dir, dvc, mocker, capsys):
tmp_dir.gen("foo", "foo")
mocker.patch(
"dvc_data.checkout.test_links",
"dvc_data.hashfile.checkout.test_links",
return_value=[],
)
dvc.add("foo")
@@ -903,6 +904,10 @@ def test_add_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
remote: testremote
meta: some metadata
"""
@@ -916,6 +921,10 @@ def test_add_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
remote: testremote
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
@@ -1161,3 +1170,23 @@ def test_add_ignore_duplicated_targets(tmp_dir, dvc, capsys):
_, err = capsys.readouterr()
assert len(stages) == 3
assert "ignoring duplicated targets: foo, bar" in err
def test_add_with_annotations(M, tmp_dir, dvc):
tmp_dir.gen("foo", "foo")
annot = {
"desc": "foo desc",
"labels": ["l1", "l2"],
"type": "t1",
"meta": {"key": "value"},
}
(stage,) = dvc.add("foo", **annot)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
# try to selectively update/overwrite some annotations
annot = {**annot, "type": "t2"}
(stage,) = dvc.add("foo", type="t2")
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
+1 -1
View File
@@ -90,7 +90,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
class TestCheckoutCorruptedCacheDir(TestDvc):
def test(self):
from dvc_data import load
from dvc_data.hashfile import load
# NOTE: using 'copy' so that cache and link don't have same inode
ret = main(["config", "cache.type", "copy"])
+14
View File
@@ -47,6 +47,13 @@ def test_commit_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key1: value1
key2: value2
remote: testremote
meta: some metadata
"""
@@ -61,6 +68,13 @@ def test_commit_preserve_fields(tmp_dir, dvc):
outs:
- path: foo # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key1: value1
key2: value2
remote: testremote
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
+1 -1
View File
@@ -13,8 +13,8 @@ from dvc.testing.test_remote import ( # noqa, pylint: disable=unused-import
TestRemote,
)
from dvc.utils.fs import remove
from dvc_data.db.local import LocalHashFileDB
from dvc_data.hashfile.db import HashFileDB
from dvc_data.hashfile.db.local import LocalHashFileDB
def test_cloud_cli(tmp_dir, dvc, remote, mocker):
+31
View File
@@ -0,0 +1,31 @@
from operator import itemgetter
from dvc.repo.data import ls
def test_data_ls(tmp_dir, dvc):
assert not list(ls(dvc))
tmp_dir.dvc_gen("bar", "bar")
tmp_dir.gen("foo", "foo")
dvc.add(
"foo",
meta={"key": "value"},
labels=["l1", "l2"],
type="t1",
desc="foo",
)
foo_entry = {
"path": "foo",
"desc": "foo",
"type": "t1",
"labels": ["l1", "l2"],
"meta": {"key": "value"},
}
assert sorted(ls(dvc), key=itemgetter("path")) == [
{"path": "bar"},
foo_entry,
]
assert list(ls(dvc, targets=["foo"])) == [foo_entry]
assert list(ls(dvc, targets=["foo"], recursive=True)) == [foo_entry]
+2 -2
View File
@@ -4,7 +4,7 @@ from os.path import join
import pytest
from dvc.repo import Repo
from dvc.repo.data import _transform_git_paths_to_dvc
from dvc.repo.data import _transform_git_paths_to_dvc, posixpath_to_os_path
from dvc.testing.tmp_dir import make_subrepo
from dvc.utils.fs import remove
@@ -31,7 +31,7 @@ def test_git_to_dvc_path_wdir_transformation(tmp_dir, scm, path):
subdir.gen(struct)
_, _, untracked = scm.status(untracked_files="all")
# make order independent of the platforms for easier test assertions
untracked = sorted(untracked, reverse=True)
untracked = sorted(map(posixpath_to_os_path, untracked), reverse=True)
assert _transform_git_paths_to_dvc(dvc, untracked) == [
"file",
join("dir", "foo"),
+30 -1
View File
@@ -3,6 +3,7 @@ import textwrap
import pytest
from dvc.annotations import Annotation
from dvc.dvcfile import (
PIPELINE_FILE,
PIPELINE_LOCK,
@@ -335,7 +336,7 @@ def test_dvcfile_dump_preserves_desc(tmp_dir, dvc, run_copy):
(tmp_dir / dvcfile.path).dump(data)
assert stage.desc == stage_desc
stage.outs[0].desc = out_desc
stage.outs[0].annot.desc = out_desc
dvcfile.dump(stage)
loaded = dvcfile._load()[0]
assert loaded == data
@@ -382,3 +383,31 @@ def test_dvcfile_try_dumping_parametrized_stage(tmp_dir, dvc, data, name):
dvcfile.dump(stage)
assert str(exc.value) == f"cannot dump a parametrized stage: '{name}'"
def test_dvcfile_load_dump_stage_with_desc_meta(tmp_dir, dvc):
data = {
"stages": {
"stage1": {
"cmd": "cp foo bar",
"desc": "stage desc",
"meta": {"key1": "value1", "key2": "value2"},
"deps": ["foo"],
"outs": [
{"bar": {"desc": "bar desc", "meta": {"key": "value"}}}
],
}
}
}
(tmp_dir / "dvc.yaml").dump(data)
stage = dvc.stage.load_one(name="stage1")
assert stage.meta == {"key1": "value1", "key2": "value2"}
assert stage.desc == "stage desc"
assert stage.outs[0].annot == Annotation(
desc="bar desc", meta={"key": "value"}
)
# sanity check
stage.dump()
assert (tmp_dir / "dvc.yaml").parse() == data
+2 -2
View File
@@ -7,8 +7,8 @@ from dvc.external_repo import CLONES, external_repo
from dvc.testing.tmp_dir import make_subrepo
from dvc.utils import relpath
from dvc.utils.fs import makedirs, remove
from dvc_data.build import build
from dvc_data.transfer import transfer
from dvc_data.hashfile.build import build
from dvc_data.hashfile.transfer import transfer
def test_external_repo(erepo_dir, mocker):
+1 -1
View File
@@ -11,7 +11,7 @@ from dvc.exceptions import CollectCacheError
from dvc.fs import LocalFileSystem
from dvc.repo import Repo as DvcRepo
from dvc.utils.fs import remove
from dvc_data.db.local import LocalHashFileDB
from dvc_data.hashfile.db.local import LocalHashFileDB
from tests.basic_env import TestDir, TestDvcGit
+1 -1
View File
@@ -10,7 +10,7 @@ from dvc.pathspec_math import PatternInfo, merge_patterns
from dvc.repo import Repo
from dvc.testing.tmp_dir import TmpDir
from dvc.types import List
from dvc_data.build import IgnoreInCollectedDirError
from dvc_data.hashfile.build import IgnoreInCollectedDirError
from dvc_data.hashfile.utils import get_mtime_and_size
+25 -1
View File
@@ -6,6 +6,7 @@ import pytest
from funcy import first
from scmrepo.git import Git
from dvc.annotations import Annotation
from dvc.config import NoRemoteError
from dvc.dvcfile import Dvcfile
from dvc.exceptions import DownloadError, PathMissingError
@@ -527,7 +528,7 @@ def test_import_with_no_exec(tmp_dir, dvc, erepo_dir):
def test_import_with_jobs(mocker, dvc, erepo_dir):
import dvc_data.transfer as otransfer
import dvc_data.hashfile.transfer as otransfer
with erepo_dir.chdir():
erepo_dir.dvc_gen(
@@ -627,3 +628,26 @@ def test_parameterized_repo(tmp_dir, dvc, scm, erepo_dir, paths):
"url": os.fspath(erepo_dir),
"rev_lock": erepo_dir.scm.get_rev(),
}
def test_import_with_annotations(M, tmp_dir, scm, dvc, erepo_dir):
with erepo_dir.chdir():
erepo_dir.dvc_gen("foo", "foo content", commit="create foo")
annot = {
"desc": "foo desc",
"labels": ["l1", "l2"],
"type": "t1",
"meta": {"key": "value"},
}
stage = dvc.imp(os.fspath(erepo_dir), "foo", "foo", no_exec=True, **annot)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
# try to selectively update/overwrite some annotations
annot = {**annot, "type": "t2"}
stage = dvc.imp(
os.fspath(erepo_dir), "foo", "foo", no_exec=True, type="t2"
)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
+63 -4
View File
@@ -5,8 +5,9 @@ from uuid import uuid4
import pytest
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dependency.base import DependencyDoesNotExistError
from dvc.dependency.base import Dependency, DependencyDoesNotExistError
from dvc.exceptions import InvalidArgumentError
from dvc.stage import Stage
from dvc.testing.test_workspace import TestImport as _TestImport
@@ -132,7 +133,7 @@ class TestImport(_TestImport):
return False
def test_import_url_preserve_meta(tmp_dir, dvc):
def test_import_url_preserve_fields(tmp_dir, dvc):
text = textwrap.dedent(
"""\
# top comment
@@ -142,6 +143,12 @@ def test_import_url_preserve_meta(tmp_dir, dvc):
outs:
- path: bar # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key: value
meta: some metadata
"""
)
@@ -160,6 +167,12 @@ def test_import_url_preserve_meta(tmp_dir, dvc):
outs:
- path: bar # out comment
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key: value
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
meta: some metadata
@@ -281,11 +294,57 @@ def test_import_url_no_download(tmp_dir, dvc, local_workspace):
assert not dst.exists()
out = stage.outs[0]
assert out.hash_info.value is None
assert out.hash_info.name is None
assert not out.hash_info
assert out.meta.size is None
status = dvc.status()
assert status["file.dvc"] == [
{"changed outs": {"file": "deleted"}},
]
def test_imp_url_with_annotations(M, tmp_dir, dvc, local_workspace):
local_workspace.gen("foo", "foo")
annot = {
"desc": "foo desc",
"labels": ["l1", "l2"],
"type": "t1",
"meta": {"key": "value"},
}
stage = dvc.imp_url(
"remote://workspace/foo",
os.fspath(tmp_dir / "foo"),
no_exec=True,
**annot,
)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
# try to selectively update/overwrite some annotations
annot = {**annot, "type": "t2"}
stage = dvc.imp_url(
"remote://workspace/foo",
os.fspath(tmp_dir / "foo"),
no_exec=True,
type="t2",
)
assert stage.outs[0].annot == Annotation(**annot)
assert (tmp_dir / "foo.dvc").parse() == M.dict(outs=[M.dict(**annot)])
def test_import_url_fs_config(tmp_dir, dvc, workspace, mocker):
import dvc.fs as dvc_fs
workspace.gen("foo", "foo")
url = "remote://workspace/foo"
get_fs_config = mocker.spy(dvc_fs, "get_fs_config")
dep_init = mocker.spy(Dependency, "__init__")
dvc.imp_url(url, fs_config={"jobs": 42})
dep_init_kwargs = dep_init.call_args[1]
assert dep_init_kwargs.get("fs_config") == {"jobs": 42}
assert get_fs_config.call_args_list[0][1] == {"url": "foo"}
assert get_fs_config.call_args_list[1][1] == {"url": url, "jobs": 42}
assert get_fs_config.call_args_list[2][1] == {"name": "workspace"}
+1 -1
View File
@@ -43,7 +43,7 @@ def test_cache(tmp_dir, dvc):
def test_cache_load_bad_dir_cache(tmp_dir, dvc):
from dvc_data import load
from dvc_data.hashfile import load
dir_hash = "123.dir"
fname = os.fspath(dvc.odb.local.oid_to_path(dir_hash))
+8 -4
View File
@@ -139,8 +139,8 @@ def test_upper_case_remote(tmp_dir, dvc, local_cloud):
def test_dir_hash_should_be_key_order_agnostic(tmp_dir, dvc):
from dvc_data.build import build
from dvc_data.objects.tree import Tree
from dvc_data.hashfile.build import build
from dvc_data.hashfile.tree import Tree
tmp_dir.gen({"data": {"1": "1 content", "2": "2 content"}})
@@ -150,7 +150,9 @@ def test_dir_hash_should_be_key_order_agnostic(tmp_dir, dvc):
[{"relpath": "1", "md5": "1"}, {"relpath": "2", "md5": "2"}]
)
tree.digest()
with patch("dvc_data.build._build_tree", return_value=(None, tree)):
with patch(
"dvc_data.hashfile.build._build_tree", return_value=(None, tree)
):
_, _, obj = build(dvc.odb.local, path, dvc.odb.local.fs, "md5")
hash1 = obj.hash_info
@@ -161,7 +163,9 @@ def test_dir_hash_should_be_key_order_agnostic(tmp_dir, dvc):
[{"md5": "1", "relpath": "1"}, {"md5": "2", "relpath": "2"}]
)
tree.digest()
with patch("dvc_data.build._build_tree", return_value=(None, tree)):
with patch(
"dvc_data.hashfile.build._build_tree", return_value=(None, tree)
):
_, _, obj = build(dvc.odb.local, path, dvc.odb.local.fs, "md5")
hash2 = obj.hash_info
+12
View File
@@ -972,6 +972,12 @@ def test_run_force_preserves_comments_and_meta(tmp_dir, dvc, run_copy):
# comment preserved
- path: bar
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key: value
meta:
name: copy-foo-bar
"""
@@ -996,6 +1002,12 @@ def test_run_force_preserves_comments_and_meta(tmp_dir, dvc, run_copy):
# comment preserved
- path: bar
desc: out desc
type: mytype
labels:
- label1
- label2
meta:
key: value
md5: acbd18db4cc2f85cedef654fccc4a4d8
size: 3
meta:
+61 -30
View File
@@ -3,6 +3,7 @@ import tempfile
import pytest
from dvc.annotations import Annotation
from dvc.cli import main
from dvc.dvcfile import SingleStageFile
from dvc.fs import LocalFileSystem
@@ -10,6 +11,8 @@ from dvc.output import Output
from dvc.repo import Repo, lock_repo
from dvc.stage import PipelineStage, Stage
from dvc.stage.run import run_stage
from dvc.stage.utils import compute_md5
from dvc.utils import dict_md5
from dvc.utils.serialize import dump_yaml, load_yaml
from dvc.utils.strictyaml import YAMLValidationError
from tests.basic_env import TestDvc
@@ -164,40 +167,68 @@ def test_md5_ignores_comments(tmp_dir, dvc):
assert not new_stage.changed_stage()
def test_meta_is_preserved(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen("foo", "foo content")
# Add meta to DVC-file
data = (tmp_dir / stage.path).parse()
data["meta"] = {"custom_key": 42}
(tmp_dir / stage.path).dump(data)
# Loading and dumping to test that it works and meta is retained
dvcfile = SingleStageFile(dvc, stage.path)
new_stage = dvcfile.stage
dvcfile.dump(new_stage)
new_data = (tmp_dir / stage.path).parse()
assert new_data["meta"] == data["meta"]
def test_md5_ignores_annotations(tmp_dir, dvc):
data = {
"desc": "stage desc",
"meta": {"key1": "value1", "key2": "value2"},
"outs": [
{
"md5": "d3b07384d113edec49eaa6238ad5ff00",
"size": 4,
"path": "foo",
"desc": "foo desc",
"type": "mytype",
"labels": ["get-started", "dataset-registry"],
"meta": {"key1": "value1"},
}
],
}
(tmp_dir / "foo.dvc").dump(data)
stage = dvc.stage.load_one("foo.dvc")
assert compute_md5(stage) == "1822617147b53ae6f9eb4b3c87c0b6f3"
assert (
dict_md5(
{
"outs": [
{"md5": "d3b07384d113edec49eaa6238ad5ff00", "path": "foo"}
]
}
)
== "1822617147b53ae6f9eb4b3c87c0b6f3"
)
def test_desc_is_preserved(tmp_dir, dvc):
(stage,) = tmp_dir.dvc_gen("foo", "foo content")
def test_meta_desc_is_preserved(tmp_dir, dvc):
data = {
"desc": "stage desc",
"meta": {"key1": "value1", "key2": "value2"},
"outs": [
{
"md5": "d3b07384d113edec49eaa6238ad5ff00",
"size": 4,
"path": "foo",
"desc": "foo desc",
"type": "mytype",
"labels": ["get-started", "dataset-registry"],
"meta": {"key": "value"},
}
],
}
(tmp_dir / "foo.dvc").dump(data)
stage = dvc.stage.load_one("foo.dvc")
data = (tmp_dir / stage.path).parse()
stage_desc = "test stage description"
out_desc = "test out description"
data["desc"] = stage_desc
data["outs"][0]["desc"] = out_desc
(tmp_dir / stage.path).dump(data)
assert stage.meta == {"key1": "value1", "key2": "value2"}
assert stage.desc == "stage desc"
assert stage.outs[0].annot == Annotation(
desc="foo desc",
type="mytype",
labels=["get-started", "dataset-registry"],
meta={"key": "value"},
)
dvcfile = SingleStageFile(dvc, stage.path)
new_stage = dvcfile.stage
dvcfile.dump(new_stage)
new_data = (tmp_dir / stage.path).parse()
assert new_data["desc"] == stage_desc
assert new_data["outs"][0]["desc"] == out_desc
# sanity check
stage.dump()
assert (tmp_dir / "foo.dvc").parse() == data
def test_parent_repo_collect_stages(tmp_dir, scm, dvc):
+10 -25
View File
@@ -116,23 +116,19 @@ def stages(tmp_dir, run_copy):
def test_collect_not_a_group_stage_with_group_flag(tmp_dir, dvc, stages):
assert set(dvc.stage.collect("copy-bar-foobar", accept_group=True)) == {
assert set(dvc.stage.collect("copy-bar-foobar")) == {
stages["copy-bar-foobar"]
}
assert set(
dvc.stage.collect("copy-bar-foobar", accept_group=True, with_deps=True)
) == {
assert set(dvc.stage.collect("copy-bar-foobar", with_deps=True)) == {
stages["copy-bar-foobar"],
stages["copy-foo-bar"],
stages["foo-generate"],
}
assert set(dvc.stage.collect_granular("copy-bar-foobar")) == {
(stages["copy-bar-foobar"], None)
}
assert set(
dvc.stage.collect_granular("copy-bar-foobar", accept_group=True)
) == {(stages["copy-bar-foobar"], None)}
assert set(
dvc.stage.collect_granular(
"copy-bar-foobar", accept_group=True, with_deps=True
)
dvc.stage.collect_granular("copy-bar-foobar", with_deps=True)
) == {
(stages["copy-bar-foobar"], None),
(stages["copy-foo-bar"], None),
@@ -153,11 +149,8 @@ def test_collect_generated(tmp_dir, dvc):
assert len(all_stages) == 5
assert set(dvc.stage.collect()) == all_stages
assert set(dvc.stage.collect("build", accept_group=True)) == all_stages
assert (
set(dvc.stage.collect("build", accept_group=True, with_deps=True))
== all_stages
)
assert set(dvc.stage.collect("build")) == all_stages
assert set(dvc.stage.collect("build", with_deps=True)) == all_stages
assert set(dvc.stage.collect("build*", glob=True)) == all_stages
assert (
set(dvc.stage.collect("build*", glob=True, with_deps=True))
@@ -165,17 +158,9 @@ def test_collect_generated(tmp_dir, dvc):
)
stages_info = {(stage, None) for stage in all_stages}
assert set(dvc.stage.collect_granular("build")) == stages_info
assert (
set(dvc.stage.collect_granular("build", accept_group=True))
== stages_info
)
assert (
set(
dvc.stage.collect_granular(
"build", accept_group=True, with_deps=True
)
)
== stages_info
set(dvc.stage.collect_granular("build", with_deps=True)) == stages_info
)
+51
View File
@@ -178,3 +178,54 @@ def test_compose_and_dump(tmp_dir, suffix, overrides, expected):
output_file = tmp_dir / f"params.{suffix}"
compose_and_dump(output_file, config_dir, config_name, overrides)
assert output_file.parse() == expected
@pytest.mark.parametrize(
"overrides, expected",
[
(
{"params.yaml": ["foo=1,2", "bar=3,4"]},
[
{"params.yaml": ["foo=1", "bar=3"]},
{"params.yaml": ["foo=1", "bar=4"]},
{"params.yaml": ["foo=2", "bar=3"]},
{"params.yaml": ["foo=2", "bar=4"]},
],
),
(
{"params.yaml": ["foo=choice(1,2)"]},
[{"params.yaml": ["foo=1"]}, {"params.yaml": ["foo=2"]}],
),
(
{"params.yaml": ["foo=range(1, 3)"]},
[{"params.yaml": ["foo=1"]}, {"params.yaml": ["foo=2"]}],
),
(
{"params.yaml": ["foo=1,2"], "others.yaml": ["bar=3"]},
[
{"params.yaml": ["foo=1"], "others.yaml": ["bar=3"]},
{"params.yaml": ["foo=2"], "others.yaml": ["bar=3"]},
],
),
(
{"params.yaml": ["foo=1,2"], "others.yaml": ["bar=3,4"]},
[
{"params.yaml": ["foo=1"], "others.yaml": ["bar=3"]},
{"params.yaml": ["foo=1"], "others.yaml": ["bar=4"]},
{"params.yaml": ["foo=2"], "others.yaml": ["bar=3"]},
{"params.yaml": ["foo=2"], "others.yaml": ["bar=4"]},
],
),
],
)
def test_hydra_sweeps(overrides, expected):
from dvc.utils.hydra import get_hydra_sweeps
assert get_hydra_sweeps(overrides) == expected
def test_invalid_sweep():
from dvc.utils.hydra import get_hydra_sweeps
with pytest.raises(InvalidArgumentError):
get_hydra_sweeps({"params.yaml": ["foo=glob(*)"]})
+1 -1
View File
@@ -4,7 +4,7 @@ import pytest
from funcy import raiser
from dvc.cli import main
from dvc_data.build import IgnoreInCollectedDirError
from dvc_data.hashfile.build import IgnoreInCollectedDirError
from dvc_data.hashfile.cache import DiskError
from dvc_objects.fs.base import FileSystem, RemoteMissingDepsError
+6
View File
@@ -37,6 +37,9 @@ def test_add(mocker, dvc):
remote=None,
to_remote=False,
desc="stage description",
type=None,
labels=None,
meta=None,
jobs=None,
)
@@ -71,6 +74,9 @@ def test_add_to_remote(mocker):
remote="remote",
to_remote=True,
desc=None,
type=None,
labels=None,
meta=None,
jobs=None,
)
@@ -0,0 +1,47 @@
import pytest
from dvc.cli import parse_args
from tests.utils.asserts import called_once_with_subset
@pytest.mark.parametrize(
"func, args",
[
("add", ("add", "model.pkl")),
("imp", ("import", "https://github.com/org/repo", "model.pkl")),
("imp_url", ("import-url", "https://dvc.org/data.xml", "data.xml")),
],
ids=["add", "import", "import-url"],
)
def test_add_annotations(mocker, func, args):
mocker.patch("dvc.repo.Repo")
cli_args = parse_args(
[
*args,
"--label",
"example-get-started",
"--label",
"model-registry",
"--type",
"model",
"--desc",
"description",
"--meta",
"key1=value1",
"--meta",
"key2=value2",
]
)
_, *posargs = args
cmd = cli_args.func(cli_args)
assert cmd.run() == 0
assert called_once_with_subset(
getattr(cmd.repo, func),
posargs,
desc="description",
type="model",
labels=["example-get-started", "model-registry"],
meta={"key1": "value1", "key2": "value2"},
)
+1
View File
@@ -15,6 +15,7 @@ def _id_gen(val) -> str:
"args, key",
[
(["dag", "--show-md"], "markdown"),
(["data", "ls", "--show-md"], "markdown"),
(["diff", "--show-json"], "json"),
(["diff", "--show-md"], "markdown"),
(["experiments", "diff", "--show-json"], "json"),
+96
View File
@@ -0,0 +1,96 @@
import pytest
from dvc.cli import parse_args
from dvc.commands.data import CmdDataLs
def test_cli(mocker):
mocker.patch("dvc.repo.Repo")
ls = mocker.patch("dvc.repo.data.ls", return_value={})
show_table_spy = mocker.spy(CmdDataLs, "_show_table")
cli_args = parse_args(
[
"data",
"ls",
"--labels",
"label1,label2",
"--labels",
"label3",
"--type",
"type1,type2",
"--type",
"type3",
"target",
"--recursive",
"--md",
]
)
assert cli_args.func == CmdDataLs
cmd = cli_args.func(cli_args)
assert cmd.run() == 0
ls.assert_called_once_with(
cmd.repo,
targets=["target"],
recursive=True,
)
show_table_spy.assert_called_once_with(
{},
filter_labels={"label1", "label2", "label3"},
filter_types={"type1", "type2", "type3"},
markdown=True,
)
EXAMPLE_DATA = [
{
"path": "model.pkl",
"desc": "desc",
"type": "model",
"labels": ["get-started", "dataset-registry"],
"meta": {"key": "value", "key1": "value1"},
},
{
"path": "model.pkl",
"desc": "desc",
"type": "model",
"labels": ["get-started", "example"],
},
{"path": "unlabeled.txt"},
]
@pytest.mark.parametrize(
"markdown, expected_output",
[
(
True,
"""\
| Path | Type | Labels | Description |
|-----------|--------|------------------------------|---------------|
| model.pkl | model | get-started,dataset-registry | desc |
| model.pkl | model | get-started,example | desc |\n""",
),
(
False,
"""\
Path Type Labels Description
model.pkl model get-started,dataset-registry desc
model.pkl model get-started,example desc""",
),
],
ids=["markdown", "default"],
)
def test_ls(capsys, markdown, expected_output):
CmdDataLs._show_table(
EXAMPLE_DATA,
filter_types={"model"},
filter_labels={"get-started", "dataset-registry"},
markdown=markdown,
)
out, err = capsys.readouterr()
assert not err
out = "\n".join(line.rstrip() for line in out.splitlines())
assert out == expected_output
+9
View File
@@ -36,6 +36,9 @@ def test_import(mocker):
no_exec=False,
no_download=False,
desc="description",
type=None,
labels=None,
meta=None,
jobs=3,
)
@@ -72,6 +75,9 @@ def test_import_no_exec(mocker):
no_exec=True,
no_download=False,
desc="description",
type=None,
labels=None,
meta=None,
jobs=None,
)
@@ -108,5 +114,8 @@ def test_import_no_download(mocker):
no_exec=False,
no_download=True,
desc="description",
type=None,
labels=None,
meta=None,
jobs=None,
)
+9
View File
@@ -37,6 +37,9 @@ def test_import_url(mocker):
remote=None,
to_remote=False,
desc="description",
type=None,
labels=None,
meta=None,
jobs=4,
)
@@ -92,6 +95,9 @@ def test_import_url_no_exec_download_flags(mocker, flag, expected):
to_remote=False,
desc="description",
jobs=None,
type=None,
labels=None,
meta=None,
**expected
)
@@ -125,6 +131,9 @@ def test_import_url_to_remote(mocker):
remote="remote",
to_remote=True,
desc="description",
type=None,
labels=None,
meta=None,
jobs=None,
)
+8 -8
View File
@@ -4,7 +4,7 @@ import os
import pytest
from dvc.fs import LocalFileSystem
from dvc_data.db.local import LocalHashFileDB
from dvc_data.hashfile.db.local import LocalHashFileDB
from dvc_data.hashfile.hash_info import HashInfo
@@ -13,7 +13,7 @@ def test_status_download_optimization(mocker, dvc):
And the desired files to fetch are already on the local cache,
Don't check the existence of the desired files on the remote cache
"""
from dvc_data.status import compare_status
from dvc_data.hashfile.status import compare_status
odb = LocalHashFileDB(LocalFileSystem(), os.getcwd())
obj_ids = {
@@ -85,9 +85,9 @@ def test_set_exec_ignore_errors(tmp_dir, dvc, mocker, err):
def test_staging_file(tmp_dir, dvc):
from dvc_data import check
from dvc_data.build import build
from dvc_data.transfer import transfer
from dvc_data.hashfile import check
from dvc_data.hashfile.build import build
from dvc_data.hashfile.transfer import transfer
tmp_dir.gen("foo", "foo")
fs = LocalFileSystem()
@@ -113,9 +113,9 @@ def test_staging_file(tmp_dir, dvc):
def test_staging_dir(tmp_dir, dvc):
from dvc_data import check
from dvc_data.build import build
from dvc_data.transfer import transfer
from dvc_data.hashfile import check
from dvc_data.hashfile.build import build
from dvc_data.hashfile.transfer import transfer
tmp_dir.gen({"dir": {"foo": "foo", "bar": "bar"}})
fs = LocalFileSystem()
+1 -1
View File
@@ -6,7 +6,7 @@ import pytest
import dvc_data
from dvc.fs.data import DataFileSystem
from dvc.utils.fs import remove
from dvc_data.build import build
from dvc_data.hashfile.build import build
from dvc_data.hashfile.hash_info import HashInfo
+2 -2
View File
@@ -7,7 +7,7 @@ import pytest
from dvc.fs.dvc import DvcFileSystem
from dvc.testing.tmp_dir import make_subrepo
from dvc_data.build import build
from dvc_data.hashfile.build import build
from dvc_data.hashfile.hash_info import HashInfo
@@ -566,7 +566,7 @@ def test_get_hash_mixed_dir(tmp_dir, scm, dvc):
def test_get_hash_dirty_file(tmp_dir, dvc):
from dvc_data import check
from dvc_data.hashfile import check
from dvc_data.hashfile.hash import hash_file
tmp_dir.dvc_gen("file", "file")
+29
View File
@@ -0,0 +1,29 @@
import pytest
from dvc.annotations import Annotation
@pytest.mark.parametrize(
"kwargs",
[
{"desc": "desc", "type": "type", "labels": ["label1", "label2"]},
{"desc": "desc", "type": "type", "meta": {"key": "value"}},
],
)
def test_annotation_to_dict(kwargs):
annot = Annotation(**kwargs)
assert annot.to_dict() == kwargs
def test_annotation_update():
annot = Annotation(desc="desc", labels=["label1", "label2"])
annot.update(
labels=["label"], type="type", new="new", meta={"key": "value"}
)
assert vars(annot) == {
"desc": "desc",
"type": "type",
"labels": ["label"],
"meta": {"key": "value"},
}
+2
View File
@@ -90,6 +90,8 @@ def test_get_used_objs(exists, expected_message, mocker, caplog):
mocker.patch.object(
stage.repo.dvcignore, "check_ignore", return_value=_no_match("path")
)
stage.repo.fs.version_aware = False
stage.repo.fs.PARAM_CHECKSUM = "md5"
output = Output(stage, "path")
+1 -1
View File
@@ -3,7 +3,7 @@ import os
import pytest
from funcy import first
from dvc_data.db.index import ObjectDBIndex
from dvc_data.hashfile.db.index import ObjectDBIndex
@pytest.fixture
+1 -1
View File
@@ -5,7 +5,7 @@ from dvc_gs import GSFileSystem
from dvc_s3 import S3FileSystem
from dvc.fs import get_cloud_fs
from dvc_data.db import get_index
from dvc_data.hashfile.db import get_index
def test_remote_with_hash_jobs(dvc):
+5 -1
View File
@@ -24,6 +24,10 @@ def test_queue(tmp_dir, dvc, scm, mocker) -> LocalCeleryQueue:
"""
celery_queue = dvc.experiments.celery_queue
mocker.patch.object(celery_queue, "spawn_worker")
with _thread_worker(celery_queue.celery, concurrency=1) as worker:
with _thread_worker(
celery_queue.celery,
concurrency=1,
ping_task_timeout=30,
) as worker:
mocker.patch.object(celery_queue, "worker", return_value=worker)
yield celery_queue
+1 -1
View File
@@ -11,7 +11,7 @@ def called_once_with_subset(m: Mock, *args: Any, **kwargs: Any) -> bool:
m.assert_called_once()
m_args, m_kwargs = m.call_args
expected_args = m_args + (ANY,) * (len(m_args) - len(args))
expected_args = m_args + (ANY,) * (len(m_args) - len(args) - 1)
expected_kwargs = {k: kwargs.get(k, ANY) for k in m_kwargs}
m.assert_called_with(*expected_args, **expected_kwargs)