mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 23:09:28 +01:00
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5ae8b19a4 | ||
|
|
f2b1c41cdf | ||
|
|
ffd32dfe8a | ||
|
|
2b19c724c5 | ||
|
|
28b92c8b03 | ||
|
|
cd740fed42 | ||
|
|
5f31ed2b08 | ||
|
|
6e896d396c | ||
|
|
336c8695da | ||
|
|
3915fa26aa | ||
|
|
114fd8075c | ||
|
|
b39a888948 | ||
|
|
c1af66c0a6 | ||
|
|
75c5d17d1e | ||
|
|
ea9dd5fc3e | ||
|
|
763cbf6bd1 | ||
|
|
6f4b8c98fc | ||
|
|
a8bda305cd | ||
|
|
4f4c5b1d01 | ||
|
|
76cfe92960 | ||
|
|
79454557bb | ||
|
|
85046843ef | ||
|
|
e10b27acbd | ||
|
|
5a86586f3a | ||
|
|
d142d0d6af | ||
|
|
9b5ee43ce1 | ||
|
|
6e223633e7 | ||
|
|
29386a0dbb | ||
|
|
e027bc6a87 | ||
|
|
82efd3e780 | ||
|
|
a68dda4164 | ||
|
|
4b1adfd646 | ||
|
|
748c45655e | ||
|
|
225bfa6a19 | ||
|
|
09079d50ff | ||
|
|
5485e5662e | ||
|
|
e619b6dccd | ||
|
|
396e71f30c | ||
|
|
c6a393e3fe | ||
|
|
a72402c843 | ||
|
|
08f3197cdd | ||
|
|
1f325bc21e | ||
|
|
ca73078b80 | ||
|
|
fade6c5991 | ||
|
|
446a59c806 | ||
|
|
a370f80f90 | ||
|
|
dc13186759 | ||
|
|
ba1281a41c | ||
|
|
abb5977a38 | ||
|
|
50487e931f | ||
|
|
397f4c6f28 | ||
|
|
1f5bace250 | ||
|
|
5a68e821f2 | ||
|
|
b4d8690799 | ||
|
|
7f0c835eed | ||
|
|
c2506abf74 | ||
|
|
d938a1c700 | ||
|
|
a0b6b12a4f | ||
|
|
1b9b1f870a | ||
|
|
be1b529bdf |
@@ -14,11 +14,11 @@ jobs:
|
||||
name: run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-depth: 0
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: run benchmarks on base branch
|
||||
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
name: Build & Verify package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
@@ -23,18 +23,18 @@ jobs:
|
||||
plugin: ["dvc-s3"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
path: dvc
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
repository: iterative/${{ matrix.plugin }}
|
||||
ref: main
|
||||
path: ${{ matrix.plugin }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
@@ -118,11 +118,11 @@ jobs:
|
||||
pyv: "3.13"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
@@ -137,6 +137,7 @@ jobs:
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest ${{ matrix.pytestargs }} -n=logical --dist=worksteal --timeout=300 --durations=0
|
||||
|
||||
@@ -6,7 +6,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
@@ -22,7 +22,7 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.12.1'
|
||||
rev: 'v0.13.2'
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
|
||||
+5
-5
@@ -213,7 +213,7 @@ Community and Support
|
||||
* `Forum <https://discuss.dvc.org/>`_
|
||||
* `Discord Chat <https://dvc.org/chat>`_
|
||||
* `Email <mailto:[email protected]>`_
|
||||
* `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
||||
* `Mailing List <https://dvc.org/community#subscribe>`_
|
||||
|
||||
Copyright
|
||||
=========
|
||||
@@ -240,11 +240,11 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/vscode-dvc/main/extension/docs/overview.gif
|
||||
:alt: DVC Extension for VS Code
|
||||
|
||||
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=main
|
||||
:target: https://github.com/iterative/dvc/actions
|
||||
.. |CI| image:: https://github.com/iterative/dvc/actions/workflows/tests.yaml/badge.svg
|
||||
:target: https://github.com/iterative/dvc/actions/workflows/tests.yaml
|
||||
:alt: GHA Tests
|
||||
|
||||
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
|
||||
.. |Maintainability| image:: https://img.shields.io/codeclimate/maintainability/iterative/dvc
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
@@ -268,7 +268,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://formulae.brew.sh/formula/dvc
|
||||
:alt: Homebrew
|
||||
|
||||
.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/dvc.svg?label=conda&logo=conda-forge
|
||||
.. |Conda| image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
|
||||
:target: https://anaconda.org/conda-forge/dvc
|
||||
:alt: Conda-forge
|
||||
|
||||
|
||||
+4
-4
@@ -14,7 +14,7 @@ def viztracer_profile(
|
||||
log_async: bool = False,
|
||||
):
|
||||
try:
|
||||
import viztracer
|
||||
import viztracer # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
print("Failed to run profiler, viztracer is not installed") # noqa: T201
|
||||
yield
|
||||
@@ -37,7 +37,7 @@ def yappi_profile(
|
||||
separate_threads: Optional[bool] = False,
|
||||
):
|
||||
try:
|
||||
import yappi
|
||||
import yappi # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
print("Failed to run profiler, yappi is not installed") # noqa: T201
|
||||
yield
|
||||
@@ -79,7 +79,7 @@ def yappi_profile(
|
||||
def instrument(html_output=False):
|
||||
"""Run a statistical profiler"""
|
||||
try:
|
||||
from pyinstrument import Profiler
|
||||
from pyinstrument import Profiler # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
|
||||
yield
|
||||
@@ -123,7 +123,7 @@ def debug():
|
||||
yield
|
||||
except Exception:
|
||||
try:
|
||||
import ipdb as pdb # noqa: T100
|
||||
import ipdb as pdb # noqa: T100 # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
import pdb # type: ignore[no-redef] # noqa: T100
|
||||
pdb.post_mortem()
|
||||
|
||||
+73
-13
@@ -1,10 +1,14 @@
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.log import logger
|
||||
|
||||
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.scm import Base
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
@@ -67,6 +71,7 @@ def send(path):
|
||||
thus, the need of removing such file afterwards.
|
||||
"""
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
url = os.environ.get(DVC_ANALYTICS_ENDPOINT, "https://analytics.dvc.org")
|
||||
headers = {"content-type": "application/json"}
|
||||
@@ -81,7 +86,7 @@ def send(path):
|
||||
|
||||
try:
|
||||
requests.post(url, json=report, headers=headers, timeout=5)
|
||||
except requests.exceptions.RequestException as e:
|
||||
except RequestException as e:
|
||||
logger.trace("", exc_info=True)
|
||||
logger.debug("failed to send analytics report %s", str(e))
|
||||
|
||||
@@ -89,20 +94,62 @@ def send(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def _scm_in_use():
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import NoSCM
|
||||
def _git_remote_url(scm: Optional["Base"]) -> Optional[str]:
|
||||
from dvc.scm import Git
|
||||
|
||||
from .scm import SCM, SCMError
|
||||
if not isinstance(scm, Git):
|
||||
return None
|
||||
|
||||
from dulwich.porcelain import get_remote_repo
|
||||
|
||||
dulwich_repo = scm.dulwich.repo
|
||||
try:
|
||||
_remote, url = get_remote_repo(dulwich_repo)
|
||||
except IndexError:
|
||||
# IndexError happens when the head is detached
|
||||
_remote, url = get_remote_repo(dulwich_repo, b"origin")
|
||||
# Dulwich returns (None, "origin") if no remote set
|
||||
if (_remote, url) == (None, "origin"):
|
||||
return None
|
||||
return url
|
||||
|
||||
|
||||
def _scm_in_use(scm: Optional["Base"]) -> Optional[str]:
|
||||
return type(scm).__name__ if scm else None
|
||||
|
||||
|
||||
def _parse_git_remote_path(remote_url: str) -> str:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from scmrepo.urls import is_scp_style_url
|
||||
|
||||
parsed = urlparse(remote_url)
|
||||
# Windows Path also gets parsed with a drive letter as scheme
|
||||
# https://github.com/python/cpython/issues/86381
|
||||
if parsed.scheme and parsed.scheme in ("http", "https", "git", "ssh"):
|
||||
return parsed.path.strip("/")
|
||||
|
||||
if is_scp_style_url(remote_url):
|
||||
# handle scp-style URL
|
||||
parts = remote_url.split(":", 1)
|
||||
if len(parts) == 2:
|
||||
_, path = parts
|
||||
return path.rstrip("/")
|
||||
return remote_url
|
||||
|
||||
|
||||
def _git_remote_path_hash(scm: Optional["Base"]) -> Optional[str]:
|
||||
"""Return a hash of the git remote path."""
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
scm = SCM(root_dir=Repo.find_root())
|
||||
return type(scm).__name__
|
||||
except SCMError:
|
||||
return NoSCM.__name__
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
if remote_url := _git_remote_url(scm):
|
||||
path = _parse_git_remote_path(remote_url)
|
||||
h = hashlib.md5(path.encode("utf-8"), usedforsecurity=False) # for FIPS
|
||||
return h.hexdigest()
|
||||
except Exception: # noqa: BLE001
|
||||
logger.debug("Failed to get git remote path", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _runtime_info():
|
||||
@@ -112,6 +159,8 @@ def _runtime_info():
|
||||
from iterative_telemetry import _generate_ci_id, find_or_create_user_id
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.info import _get_remotes
|
||||
from dvc.repo import Repo
|
||||
from dvc.utils import is_binary
|
||||
|
||||
ci_id = _generate_ci_id()
|
||||
@@ -120,13 +169,24 @@ def _runtime_info():
|
||||
else:
|
||||
group_id, user_id = None, find_or_create_user_id()
|
||||
|
||||
scm = None
|
||||
remotes = None
|
||||
try:
|
||||
repo = Repo()
|
||||
scm = repo.scm
|
||||
remotes = _get_remotes(repo.config)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("failed to open repo: %s", exc)
|
||||
|
||||
return {
|
||||
"dvc_version": __version__,
|
||||
"is_binary": is_binary(),
|
||||
"scm_class": _scm_in_use(),
|
||||
"scm_class": _scm_in_use(scm),
|
||||
"system_info": _system_info(),
|
||||
"user_id": user_id,
|
||||
"group_id": group_id,
|
||||
"remotes": remotes,
|
||||
"git_remote_hash": _git_remote_path_hash(scm),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -64,3 +64,4 @@ def get(name: str) -> Union[DatachainDataset, DVCDataset, URLDataset]:
|
||||
for file in dataset.lock.files
|
||||
]
|
||||
return URLDataset(type="url", files=files, path=versioned_path)
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
+6
-5
@@ -157,6 +157,8 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
Returns:
|
||||
int: command's return code.
|
||||
"""
|
||||
from contextlib import ExitStack
|
||||
|
||||
from dvc._debug import debugtools
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
@@ -173,12 +175,11 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
logging.disable(logging.INFO)
|
||||
|
||||
args = None
|
||||
|
||||
outer_log_level = logger.level
|
||||
level = None
|
||||
stack = ExitStack()
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
level = None
|
||||
if args.quiet:
|
||||
level = logging.CRITICAL
|
||||
elif args.verbose == 1:
|
||||
@@ -187,7 +188,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
level = logging.TRACE # type: ignore[attr-defined]
|
||||
|
||||
if level is not None:
|
||||
set_loggers_level(level)
|
||||
stack.enter_context(set_loggers_level(level))
|
||||
|
||||
if level and level <= logging.DEBUG:
|
||||
from platform import platform, python_implementation, python_version
|
||||
@@ -247,7 +248,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
|
||||
return ret
|
||||
finally:
|
||||
logger.setLevel(outer_log_level)
|
||||
stack.close()
|
||||
|
||||
from dvc.repo.open_repo import clean_repos
|
||||
|
||||
|
||||
+163
-3
@@ -22,6 +22,36 @@ _dvc_compgen_exps() {
|
||||
local _dvc_exps=($(dvc exp list -q --all-commits --names-only))
|
||||
compgen -W "${_dvc_exps[*]}" -- $1
|
||||
}
|
||||
|
||||
_dvc_compgen_remotes() {
|
||||
local _dvc_remotes=($(dvc remote list | cut -d' ' -f1))
|
||||
compgen -W "${_dvc_remotes[*]}" -- $1
|
||||
}
|
||||
|
||||
_dvc_compgen_config_vars() {
|
||||
compgen -W "${_dvc_config_vars[*]}" -- $1
|
||||
}
|
||||
|
||||
_dvc_compgen_remote_config_vars() {
|
||||
local cur prev words cword remote_name filtered
|
||||
_init_completion || return
|
||||
|
||||
# last non-option word before cursor
|
||||
local nonopts=()
|
||||
for w in "${words[@]:1:$((cword-1))}"; do
|
||||
[[ $w == -* ]] || nonopts+=("$w")
|
||||
done
|
||||
|
||||
remote_name="${nonopts[-1]}"
|
||||
if [[ -n $remote_name ]]; then
|
||||
local _dvc_remote_config_vars=($(
|
||||
dvc config --available-options -q 2> /dev/null \
|
||||
| grep "^remote\\.${remote_name}\\." \
|
||||
| sed "s/^remote\\.${remote_name}\\.//"
|
||||
))
|
||||
compgen -W "${_dvc_remote_config_vars[*]}" -- $1
|
||||
fi
|
||||
}
|
||||
"""
|
||||
|
||||
ZSH_PREAMBLE = """
|
||||
@@ -55,19 +85,149 @@ _dvc_compadd_stages_and_files() {
|
||||
_dvc_compadd_exps() {
|
||||
_describe 'experiments' "($(dvc exp list -q --all-commits --names-only))"
|
||||
}
|
||||
|
||||
_dvc_compadd_remotes() {
|
||||
_describe 'remotes' "($(dvc remote list | cut -d' ' -f1))"
|
||||
}
|
||||
|
||||
_dvc_config_config_vars_for_completion() {
|
||||
dvc config --available-options -q 2> /dev/null
|
||||
}
|
||||
|
||||
_dvc_compadd_config_vars() {
|
||||
_describe 'config_vars' _dvc_config_vars
|
||||
}
|
||||
|
||||
_dvc_compadd_remote_config_vars() {
|
||||
local remote_name filtered
|
||||
|
||||
# last non-option word before cursor
|
||||
remote_name=${${words[1,CURRENT-1]##-*}[-1]}
|
||||
if [[ -n $remote_name ]]; then
|
||||
filtered=$(_dvc_config_config_vars_for_completion \\
|
||||
| grep "^remote.${remote_name}\\\\." \\
|
||||
| sed "s/^remote\\\\.${remote_name}\\\\.//"
|
||||
)
|
||||
compadd -- $=filtered
|
||||
fi
|
||||
}
|
||||
"""
|
||||
|
||||
FISH_PREAMBLE = """
|
||||
function __fish_complete_dvc_files
|
||||
__fish_complete_path | string match -re '\\*?.dvc|Dvcfile|dvc\\.yaml'
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_stages
|
||||
for line in (dvc stage list -q)
|
||||
set -l parts (string split -m1 ' ' -- $line)
|
||||
set -l name $parts[1]
|
||||
set -l desc (string trim $parts[2])
|
||||
echo -e "$name $desc"
|
||||
end
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_stages_and_files
|
||||
__fish_complete_dvc_stages
|
||||
__fish_complete_dvc_files
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_experiments
|
||||
dvc exp list -q --all-commits --names-only
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_remotes
|
||||
dvc remote list | cut -d' ' -f1
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_remote_config_vars
|
||||
# last non-option word before cursor
|
||||
set -l remote_name (commandline -opc | string match -rv '^-' | tail -n 1)
|
||||
|
||||
if test -n "$remote_name"
|
||||
dvc config --available-options -q 2>/dev/null |
|
||||
string match -re "^remote\\.$remote_name\\." |
|
||||
string replace -r "^remote\\.$remote_name\\." ''
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
PREAMBLE = {
|
||||
"bash": BASH_PREAMBLE,
|
||||
"zsh": ZSH_PREAMBLE,
|
||||
"fish": FISH_PREAMBLE,
|
||||
}
|
||||
|
||||
FILE = shtab.FILE
|
||||
DIR = shtab.DIRECTORY
|
||||
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
|
||||
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
|
||||
DVC_FILE = {
|
||||
"bash": "_dvc_compgen_DVCFiles",
|
||||
"zsh": "_dvc_compadd_DVCFiles",
|
||||
"fish": '-f -a "(__fish_complete_dvc_files)"',
|
||||
}
|
||||
STAGE = {
|
||||
"bash": "_dvc_compgen_stages",
|
||||
"zsh": "_dvc_compadd_stages",
|
||||
"fish": '-f -a "(__fish_complete_dvc_stages)"',
|
||||
}
|
||||
DVCFILES_AND_STAGE = {
|
||||
"bash": "_dvc_compgen_stages_and_files",
|
||||
"zsh": "_dvc_compadd_stages_and_files",
|
||||
"fish": '-f -a "(__fish_complete_dvc_stages_and_files)"',
|
||||
}
|
||||
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
||||
EXPERIMENT = {
|
||||
"bash": "_dvc_compgen_exps",
|
||||
"zsh": "_dvc_compadd_exps",
|
||||
"fish": '-f -a "(__fish_complete_dvc_experiments)"',
|
||||
}
|
||||
REMOTE = {
|
||||
"bash": "_dvc_compgen_remotes",
|
||||
"zsh": "_dvc_compadd_remotes",
|
||||
"fish": '-f -a "(__fish_complete_dvc_remotes)"',
|
||||
}
|
||||
CONFIG_VARS = {
|
||||
"bash": "_dvc_compgen_config_vars",
|
||||
"zsh": "_dvc_compadd_config_vars",
|
||||
"fish": '-f -a "(__fish_complete_dvc_config_vars)"',
|
||||
}
|
||||
REMOTE_CONFIG_VARS = {
|
||||
"bash": "_dvc_compgen_remote_config_vars",
|
||||
"zsh": "_dvc_compadd_remote_config_vars",
|
||||
"fish": '-f -a "(__fish_complete_dvc_remote_config_vars)"',
|
||||
}
|
||||
|
||||
|
||||
def get_preamble() -> dict[str, str]:
|
||||
from dvc.config_schema import config_vars_for_completion
|
||||
|
||||
ret: dict[str, str] = {}
|
||||
config_vars = list(config_vars_for_completion())
|
||||
|
||||
nl = "\n\t".expandtabs(4)
|
||||
config_vars_arr = f"""
|
||||
_dvc_config_vars=(
|
||||
{nl.join(config_vars)}
|
||||
)
|
||||
"""
|
||||
indent = "\t\t".expandtabs(4) # 8 spaces
|
||||
lines = (
|
||||
"\n".join(
|
||||
f"{indent}{c} \\"
|
||||
for c in config_vars[:-1] # all but last
|
||||
)
|
||||
+ "\n"
|
||||
+ f"{indent}{config_vars[-1]}"
|
||||
) # last line without backslash
|
||||
config_vars_arr_fish = f"""
|
||||
function __fish_complete_dvc_config_vars
|
||||
set -l _dvc_config_vars \\
|
||||
{lines}
|
||||
printf %s\\n $_dvc_config_vars
|
||||
end
|
||||
"""
|
||||
for shell, preamble in PREAMBLE.items():
|
||||
if shell != "fish":
|
||||
ret[shell] = config_vars_arr + preamble
|
||||
else:
|
||||
ret[shell] = config_vars_arr_fish + preamble
|
||||
return ret
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to download to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
parser.add_argument(
|
||||
"--remote-jobs",
|
||||
type=int,
|
||||
|
||||
@@ -149,7 +149,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"Remote name to set as a default in the target repository "
|
||||
"(only applicable when downloading from DVC remote)."
|
||||
),
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
get_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.ignore import CheckIgnoreResult
|
||||
|
||||
|
||||
class CmdCheckIgnore(CmdBase):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
self.ignore_filter = self.repo.dvcignore
|
||||
|
||||
def _show_results(self, result):
|
||||
if not result.match and not self.args.non_matching:
|
||||
return
|
||||
def _show_results(self, result: "CheckIgnoreResult"):
|
||||
if not result.match and not self.args.details:
|
||||
return None
|
||||
|
||||
if self.args.details:
|
||||
patterns = result.patterns
|
||||
pattern_infos = result.pattern_infos
|
||||
patterns = [str(pi) for pi in pattern_infos]
|
||||
if not patterns and self.args.non_matching:
|
||||
patterns = ["::"]
|
||||
if not self.args.all:
|
||||
patterns = patterns[-1:]
|
||||
|
||||
@@ -22,13 +30,11 @@ class CmdCheckIgnore(CmdBase):
|
||||
ui.write(pattern, result.file, sep="\t")
|
||||
else:
|
||||
ui.write(result.file)
|
||||
return bool(result.pattern_infos)
|
||||
|
||||
def _check_one_file(self, target):
|
||||
result = self.ignore_filter.check_ignore(target)
|
||||
self._show_results(result)
|
||||
if result.match:
|
||||
return 0
|
||||
return 1
|
||||
return bool(self._show_results(result))
|
||||
|
||||
def _interactive_mode(self):
|
||||
ret = 1
|
||||
@@ -39,14 +45,14 @@ class CmdCheckIgnore(CmdBase):
|
||||
break
|
||||
if not target:
|
||||
break
|
||||
if not self._check_one_file(target):
|
||||
if self._check_one_file(target):
|
||||
ret = 0
|
||||
return ret
|
||||
|
||||
def _normal_mode(self):
|
||||
ret = 1
|
||||
for target in self.args.targets:
|
||||
if not self._check_one_file(target):
|
||||
if self._check_one_file(target):
|
||||
ret = 0
|
||||
return ret
|
||||
|
||||
@@ -76,6 +82,8 @@ class CmdCheckIgnore(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
import argparse
|
||||
|
||||
ADD_HELP = "Check whether files or directories are excluded due to `.dvcignore`."
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
@@ -93,14 +101,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show the exclude patterns along with each target path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Include the target paths which don't match any pattern "
|
||||
"in the `--details` list."
|
||||
),
|
||||
"-a", "--all", action="store_true", default=False, help=argparse.SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import operator
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
@@ -30,7 +28,7 @@ class CmdCheckout(CmdBase):
|
||||
|
||||
stats, exc = None, None
|
||||
try:
|
||||
stats = self.repo.checkout(
|
||||
result = self.repo.checkout(
|
||||
targets=self.args.targets,
|
||||
with_deps=self.args.with_deps,
|
||||
force=self.args.force,
|
||||
@@ -40,14 +38,17 @@ class CmdCheckout(CmdBase):
|
||||
)
|
||||
except CheckoutError as _exc:
|
||||
exc = _exc
|
||||
stats = exc.stats
|
||||
result = exc.result
|
||||
|
||||
if self.args.summary:
|
||||
default_message = "No changes."
|
||||
msg = get_summary(sorted(stats.items(), key=operator.itemgetter(0)))
|
||||
stats = result["stats"]
|
||||
assert isinstance(stats, dict)
|
||||
msg = get_summary(stats.items())
|
||||
ui.write(msg or default_message)
|
||||
else:
|
||||
log_changes(stats)
|
||||
result.pop("stats", {})
|
||||
log_changes(result)
|
||||
|
||||
if exc:
|
||||
raise exc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.completion import PREAMBLE
|
||||
from dvc.cli.completion import get_preamble
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
from dvc.ui import ui
|
||||
@@ -8,7 +8,7 @@ from dvc.ui import ui
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
SUPPORTED_SHELLS = ["bash", "zsh"]
|
||||
SUPPORTED_SHELLS = ["bash", "zsh", "fish"]
|
||||
|
||||
|
||||
class CmdCompletion(CmdBaseNoRepo):
|
||||
@@ -17,7 +17,7 @@ class CmdCompletion(CmdBaseNoRepo):
|
||||
|
||||
shell = self.args.shell
|
||||
parser = self.args.parser
|
||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
||||
script = shtab.complete(parser, shell=shell, preamble=get_preamble())
|
||||
ui.write(script, force=True)
|
||||
return 0
|
||||
|
||||
|
||||
+14
-2
@@ -3,7 +3,7 @@ import os
|
||||
|
||||
from funcy import set_in
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -40,6 +40,15 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
self.config = Config.from_cwd(validate=False)
|
||||
|
||||
def run(self):
|
||||
if self.args.available_options:
|
||||
from dvc.config_schema import contextual_config_vars_for_completion
|
||||
|
||||
ui.write(
|
||||
"\n".join(list(contextual_config_vars_for_completion(self.config))),
|
||||
force=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
if self.args.show_origin and (self.args.value or self.args.unset):
|
||||
logger.error(
|
||||
"--show-origin can't be used together with any of these "
|
||||
@@ -212,6 +221,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=CONFIG_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--available-options", default=False, action="store_true"
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"-u",
|
||||
"--unset",
|
||||
@@ -224,7 +236,7 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
type=_name_type,
|
||||
help="Option name (section.option or remote.name.option).",
|
||||
)
|
||||
).complete = completion.CONFIG_VARS
|
||||
config_parser.add_argument("value", nargs="?", help="Option value.")
|
||||
config_parser.add_argument(
|
||||
"-l",
|
||||
|
||||
+26
-11
@@ -10,6 +10,7 @@ from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.data import GitInfo
|
||||
from dvc.repo.data import Status as DataStatus
|
||||
|
||||
|
||||
@@ -55,20 +56,15 @@ class CmdDataStatus(CmdBase):
|
||||
def _process_status(status: "DataStatus"):
|
||||
"""Flatten stage status, and filter empty stage status contents."""
|
||||
for stage, stage_status in status.items():
|
||||
items = stage_status
|
||||
if isinstance(stage_status, dict):
|
||||
items = {
|
||||
file: state
|
||||
for state, files in stage_status.items()
|
||||
for file in files
|
||||
}
|
||||
if not items:
|
||||
if not stage_status or (
|
||||
isinstance(stage_status, dict) and not any(stage_status.values())
|
||||
):
|
||||
continue
|
||||
yield stage, items
|
||||
yield stage, stage_status
|
||||
|
||||
@classmethod
|
||||
def _show_status(cls, status: "DataStatus") -> int: # noqa: C901
|
||||
git_info = status.pop("git") # type: ignore[misc]
|
||||
git_info: GitInfo = status.pop("git") # type: ignore[misc]
|
||||
result = dict(cls._process_status(status))
|
||||
if not result:
|
||||
no_changes = "No changes"
|
||||
@@ -90,7 +86,16 @@ class CmdDataStatus(CmdBase):
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [f"{state}: {file}" for file, state in stage_status.items()]
|
||||
items = [
|
||||
f"{state}: "
|
||||
+ (
|
||||
" -> ".join(change.values())
|
||||
if isinstance(change, dict)
|
||||
else change
|
||||
)
|
||||
for state, changes in stage_status.items()
|
||||
for change in changes
|
||||
]
|
||||
else:
|
||||
items = stage_status
|
||||
|
||||
@@ -111,6 +116,7 @@ class CmdDataStatus(CmdBase):
|
||||
targets=self.args.targets,
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
remote=self.args.remote,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
@@ -127,9 +133,12 @@ class CmdDataStatus(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_help = "Commands related to data management."
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(data_help, "data/status"),
|
||||
help=data_help,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
data_subparsers = data_parser.add_subparsers(
|
||||
@@ -182,6 +191,12 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to check (only applicable with --not-in-remote).",
|
||||
metavar="<name>",
|
||||
).complete = completion.REMOTE
|
||||
data_status_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
|
||||
+11
-10
@@ -22,17 +22,18 @@ class CmdDataBase(CmdBase):
|
||||
|
||||
|
||||
class CmdDataPull(CmdDataBase):
|
||||
def log_summary(self, stats):
|
||||
def log_summary(self, result):
|
||||
from dvc.commands.checkout import log_changes
|
||||
|
||||
log_changes(stats)
|
||||
stats = result.pop("stats", {})
|
||||
log_changes(result)
|
||||
super().log_summary(stats)
|
||||
|
||||
def run(self):
|
||||
from dvc.exceptions import CheckoutError, DvcException
|
||||
|
||||
try:
|
||||
stats = self.repo.pull(
|
||||
result = self.repo.pull(
|
||||
targets=self.args.targets,
|
||||
jobs=self.args.jobs,
|
||||
remote=self.args.remote,
|
||||
@@ -46,10 +47,10 @@ class CmdDataPull(CmdDataBase):
|
||||
glob=self.args.glob,
|
||||
allow_missing=self.args.allow_missing,
|
||||
)
|
||||
self.log_summary(stats)
|
||||
self.log_summary(result)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
if stats := getattr(exc, "stats", {}):
|
||||
self.log_summary(stats)
|
||||
if result := getattr(exc, "result", {}):
|
||||
self.log_summary(result)
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
|
||||
@@ -149,7 +150,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
pull_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -224,7 +225,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
push_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -286,7 +287,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
fetch_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -379,7 +380,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to compare local cache to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
status_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
|
||||
@@ -83,7 +83,7 @@ class CmdDatasetUpdate(CmdBase):
|
||||
return CmdDatasetAdd.display(name, new, action)
|
||||
if dataset == new:
|
||||
ui.write("[yellow]Nothing to update[/]", styled=True)
|
||||
return
|
||||
return None
|
||||
|
||||
assert new.lock
|
||||
|
||||
@@ -112,6 +112,7 @@ class CmdDatasetUpdate(CmdBase):
|
||||
assert new.type == "url"
|
||||
stats = diff_files(dataset.lock.files, new.lock.files)
|
||||
log_changes(stats)
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
from difflib import get_close_matches
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
du_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import argparse
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -71,7 +71,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
dest="dvc_remote",
|
||||
metavar="<name>",
|
||||
help="Name of the DVC remote to use when pulling cached outputs.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
experiments_pull_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
|
||||
@@ -111,7 +111,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
dest="dvc_remote",
|
||||
metavar="<name>",
|
||||
help="Name of the DVC remote to use when pushing cached outputs.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
experiments_push_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import argparse
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Collection, Iterable
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -59,17 +59,16 @@ def baseline_styler(typ):
|
||||
def show_experiments(
|
||||
td: "TabularData",
|
||||
headers: dict[str, Iterable[str]],
|
||||
keep=None,
|
||||
drop=None,
|
||||
keep: Collection[str] = (),
|
||||
drop: Collection[str] = (),
|
||||
pager=True,
|
||||
csv=False,
|
||||
markdown=False,
|
||||
**kwargs,
|
||||
):
|
||||
if keep:
|
||||
for col in td.keys(): # noqa: SIM118
|
||||
if re.match(keep, col):
|
||||
td.protect(col)
|
||||
keep_re = re.compile("|".join(keep))
|
||||
td.protect(*(col for col in td.keys() if keep_re.match(col))) # noqa: SIM118
|
||||
|
||||
for col in ("State", "Executor"):
|
||||
if td.is_empty(col):
|
||||
@@ -110,8 +109,9 @@ def show_experiments(
|
||||
td.drop_duplicates("cols", ignore_empty=False)
|
||||
|
||||
cols_to_drop = set()
|
||||
if drop is not None:
|
||||
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
|
||||
if drop:
|
||||
drop_re = re.compile("|".join(drop))
|
||||
cols_to_drop = {col for col in td.keys() if drop_re.match(col)} # noqa: SIM118
|
||||
td.drop(*cols_to_drop)
|
||||
|
||||
td.render(
|
||||
@@ -150,6 +150,7 @@ class CmdExperimentsShow(CmdBase):
|
||||
all_commits=self.args.all_commits,
|
||||
hide_queued=self.args.hide_queued,
|
||||
hide_failed=self.args.hide_failed,
|
||||
hide_workspace=self.args.hide_workspace,
|
||||
revs=self.args.rev,
|
||||
num=self.args.num,
|
||||
sha_only=self.args.sha,
|
||||
@@ -236,11 +237,15 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--drop",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Remove the columns matching the specified regex pattern.",
|
||||
metavar="<regex_pattern>",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--keep",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Preserve the columns matching the specified regex pattern.",
|
||||
metavar="<regex_pattern>",
|
||||
)
|
||||
@@ -279,6 +284,12 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Hide queued experiments in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--hide-workspace",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Hide workspace row in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -188,7 +188,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to collect garbage in",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
gc_parser.add_argument(
|
||||
"--skip-failed",
|
||||
action="store_true",
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
get_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
import_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
@@ -73,7 +73,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to download to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
|
||||
+13
-4
@@ -42,7 +42,7 @@ class CmdInit(CmdBaseNoRepo):
|
||||
|
||||
try:
|
||||
with Repo.init(
|
||||
".",
|
||||
self.args.directory,
|
||||
no_scm=self.args.no_scm,
|
||||
force=self.args.force,
|
||||
subdir=self.args.subdir,
|
||||
@@ -57,10 +57,11 @@ class CmdInit(CmdBaseNoRepo):
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
"""Setup parser for `dvc init`."""
|
||||
INIT_HELP = "Initialize DVC in the current directory."
|
||||
INIT_HELP = "Initialize DVC repository."
|
||||
INIT_DESCRIPTION = (
|
||||
"Initialize DVC in the current directory. Expects directory\n"
|
||||
"to be a Git repository unless --no-scm option is specified."
|
||||
"Initialize DVC repository in the given directory (defaults to the current "
|
||||
"working directory).\n"
|
||||
"Expects directory to be a Git repository unless --no-scm option is specified."
|
||||
)
|
||||
|
||||
init_parser = subparsers.add_parser(
|
||||
@@ -70,6 +71,14 @@ def add_parser(subparsers, parent_parser):
|
||||
help=INIT_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"directory",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help=(
|
||||
"Directory to initialize DVC in. Defaults to the current working directory."
|
||||
),
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--no-scm",
|
||||
action="store_true",
|
||||
|
||||
@@ -250,7 +250,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
list_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.ui import ui
|
||||
@@ -210,7 +210,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOTE_DEFAULT_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_default_parser.add_argument("name", nargs="?", help="Name of the remote")
|
||||
remote_default_parser.add_argument(
|
||||
"name", nargs="?", help="Name of the remote"
|
||||
).complete = completion.REMOTE
|
||||
remote_default_parser.add_argument(
|
||||
"-u",
|
||||
"--unset",
|
||||
@@ -228,8 +230,12 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOTE_MODIFY_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_modify_parser.add_argument("name", help="Name of the remote")
|
||||
remote_modify_parser.add_argument("option", help="Name of the option to modify.")
|
||||
remote_modify_parser.add_argument(
|
||||
"name", help="Name of the remote"
|
||||
).complete = completion.REMOTE
|
||||
remote_modify_parser.add_argument(
|
||||
"option", help="Name of the option to modify."
|
||||
).complete = completion.REMOTE_CONFIG_VARS
|
||||
remote_modify_parser.add_argument(
|
||||
"value", nargs="?", help="(optional) Value of the option."
|
||||
)
|
||||
@@ -260,7 +266,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOTE_REMOVE_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_remove_parser.add_argument("name", help="Name of the remote to remove.")
|
||||
remote_remove_parser.add_argument(
|
||||
"name", help="Name of the remote to remove."
|
||||
).complete = completion.REMOTE
|
||||
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
|
||||
REMOTE_RENAME_HELP = "Rename a DVC remote"
|
||||
remote_rename_parser = remote_subparsers.add_parser(
|
||||
|
||||
@@ -71,7 +71,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
return 0
|
||||
|
||||
if self.args.quiet:
|
||||
return bool(st)
|
||||
return int(bool(st))
|
||||
|
||||
if st:
|
||||
self._show(st, indent)
|
||||
|
||||
@@ -74,7 +74,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to perform updates to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
update_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
|
||||
@@ -139,6 +139,7 @@ class Config(dict):
|
||||
return global_config_dir()
|
||||
if level == "system":
|
||||
return system_config_dir()
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def files(self) -> dict[str, str]:
|
||||
|
||||
+203
-152
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import once, walk_values
|
||||
@@ -10,6 +11,7 @@ from voluptuous import (
|
||||
Exclusive,
|
||||
Invalid,
|
||||
Lower,
|
||||
Marker,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
@@ -17,6 +19,9 @@ from voluptuous import (
|
||||
|
||||
from dvc.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
Bool = All(
|
||||
@@ -55,23 +60,27 @@ def Choices(*choices): # noqa: N802
|
||||
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
|
||||
|
||||
|
||||
def _get_schema_from_url(url: str) -> str:
|
||||
parsed = urlparse(url)
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
|
||||
return ""
|
||||
if not parsed.netloc:
|
||||
return ""
|
||||
return parsed.scheme
|
||||
|
||||
|
||||
def ByUrl(mapping): # noqa: N802
|
||||
schemas = walk_values(Schema, mapping)
|
||||
schemas: dict[str, Schema] = walk_values(Schema, mapping)
|
||||
|
||||
def validate(data):
|
||||
if "url" not in data:
|
||||
raise Invalid("expected 'url'")
|
||||
|
||||
parsed = urlparse(data["url"])
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if parsed.scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
|
||||
|
||||
return schemas[parsed.scheme](data)
|
||||
scheme = _get_schema_from_url(data["url"])
|
||||
if scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {scheme}://")
|
||||
return schemas[scheme](data)
|
||||
|
||||
return validate
|
||||
|
||||
@@ -112,17 +121,19 @@ class FeatureSchema(Schema):
|
||||
return ret
|
||||
|
||||
|
||||
DEPRECATED = "==DEPRECATED=="
|
||||
|
||||
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("no_traverse", description=DEPRECATED): Bool, # obsoleted
|
||||
Optional("version_aware"): Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
Optional("protected", default=False): Bool, # obsoleted
|
||||
Optional("protected", default=False, description=DEPRECATED): Bool, # obsoleted
|
||||
"shared": All(Lower, Choices("group")),
|
||||
Optional("slow_link_warning", default=True): Bool,
|
||||
Optional("verify", default=False): Bool,
|
||||
@@ -152,6 +163,127 @@ WEBDAV_COMMON = {
|
||||
Optional("verify", default=False): Bool,
|
||||
}
|
||||
|
||||
REMOTE_SCHEMAS = {
|
||||
"": LOCAL_COMMON | REMOTE_COMMON,
|
||||
"s3": {
|
||||
"region": str,
|
||||
"profile": str,
|
||||
"credentialpath": str,
|
||||
"configpath": str,
|
||||
"endpointurl": str,
|
||||
"access_key_id": str,
|
||||
"secret_access_key": str,
|
||||
"session_token": str,
|
||||
Optional(
|
||||
"listobjects", default=False, description=DEPRECATED
|
||||
): Bool, # obsoleted
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"sse_customer_algorithm": str,
|
||||
"sse_customer_key": str,
|
||||
"acl": str,
|
||||
"grant_read": str,
|
||||
"grant_read_acp": str,
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
"cache_regions": bool,
|
||||
"read_timeout": Coerce(int),
|
||||
"connect_timeout": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"ssh": {
|
||||
"type": supported_cache_type,
|
||||
"port": Coerce(int),
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"hdfs": {
|
||||
"user": str,
|
||||
"kerb_ticket": str,
|
||||
"replication": int,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"webhdfs": {
|
||||
"kerberos": Bool,
|
||||
"kerberos_principal": str,
|
||||
"proxy_to": str,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"token": str,
|
||||
"use_https": Bool,
|
||||
"user": str,
|
||||
"password": str,
|
||||
"data_proxy_target": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"azure": {
|
||||
"connection_string": str,
|
||||
"sas_token": str,
|
||||
"account_name": str,
|
||||
"account_key": str,
|
||||
"tenant_id": str,
|
||||
"client_id": str,
|
||||
"client_secret": str,
|
||||
"allow_anonymous_login": Bool,
|
||||
"exclude_environment_credential": Bool,
|
||||
"exclude_visual_studio_code_credential": Bool,
|
||||
"exclude_shared_token_cache_credential": Bool,
|
||||
"exclude_managed_identity_credential": Bool,
|
||||
Optional("verify", default=False): Bool,
|
||||
"timeout": Coerce(int),
|
||||
"read_timeout": Coerce(int),
|
||||
"connection_timeout": Coerce(int),
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"oss": {
|
||||
"oss_key_id": str,
|
||||
"oss_key_secret": str,
|
||||
"oss_endpoint": str,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gdrive": {
|
||||
"profile": str,
|
||||
"gdrive_use_service_account": Bool,
|
||||
"gdrive_client_id": str,
|
||||
"gdrive_client_secret": str,
|
||||
"gdrive_user_credentials_file": str,
|
||||
"gdrive_service_account_user_email": str,
|
||||
"gdrive_service_account_json_file_path": str,
|
||||
Optional("gdrive_trash_only", default=False): Bool,
|
||||
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"http": HTTP_COMMON | REMOTE_COMMON,
|
||||
"https": HTTP_COMMON | REMOTE_COMMON,
|
||||
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"remote": {str: object}, # Any of the above options are valid
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
"core": {
|
||||
"remote": Lower,
|
||||
@@ -161,152 +293,37 @@ SCHEMA = {
|
||||
Optional("hardlink_lock", default=False): Bool,
|
||||
Optional("no_scm", default=False): Bool,
|
||||
Optional("autostage", default=False): Bool,
|
||||
Optional("experiments"): Bool, # obsoleted
|
||||
Optional("experiments", description=DEPRECATED): Bool, # obsoleted
|
||||
Optional("check_update", default=True): Bool,
|
||||
"site_cache_dir": str,
|
||||
"machine": Lower,
|
||||
},
|
||||
"cache": {
|
||||
"local": str, # obsoleted
|
||||
"s3": str, # obsoleted
|
||||
"gs": str, # obsoleted
|
||||
"hdfs": str, # obsoleted
|
||||
"webhdfs": str, # obsoleted
|
||||
"ssh": str, # obsoleted
|
||||
"azure": str, # obsoleted
|
||||
Marker("local", description=DEPRECATED): str, # obsoleted
|
||||
Marker("s3", description=DEPRECATED): str, # obsoleted
|
||||
Marker("gs", description=DEPRECATED): str, # obsoleted
|
||||
Marker("hdfs", description=DEPRECATED): str, # obsoleted
|
||||
Marker("webhdfs", description=DEPRECATED): str, # obsoleted
|
||||
Marker("ssh", description=DEPRECATED): str, # obsoleted
|
||||
Marker("azure", description=DEPRECATED): str, # obsoleted
|
||||
# This is for default local cache
|
||||
"dir": str,
|
||||
**LOCAL_COMMON,
|
||||
},
|
||||
"remote": {
|
||||
str: ByUrl(
|
||||
{
|
||||
"": LOCAL_COMMON | REMOTE_COMMON,
|
||||
"s3": {
|
||||
"region": str,
|
||||
"profile": str,
|
||||
"credentialpath": str,
|
||||
"configpath": str,
|
||||
"endpointurl": str,
|
||||
"access_key_id": str,
|
||||
"secret_access_key": str,
|
||||
"session_token": str,
|
||||
Optional("listobjects", default=False): Bool, # obsoleted
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"sse_customer_algorithm": str,
|
||||
"sse_customer_key": str,
|
||||
"acl": str,
|
||||
"grant_read": str,
|
||||
"grant_read_acp": str,
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
"cache_regions": bool,
|
||||
"read_timeout": Coerce(int),
|
||||
"connect_timeout": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"ssh": {
|
||||
"type": supported_cache_type,
|
||||
"port": Coerce(int),
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"hdfs": {
|
||||
"user": str,
|
||||
"kerb_ticket": str,
|
||||
"replication": int,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"webhdfs": {
|
||||
"kerberos": Bool,
|
||||
"kerberos_principal": str,
|
||||
"proxy_to": str,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"token": str,
|
||||
"use_https": Bool,
|
||||
"user": str,
|
||||
"password": str,
|
||||
"data_proxy_target": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"azure": {
|
||||
"connection_string": str,
|
||||
"sas_token": str,
|
||||
"account_name": str,
|
||||
"account_key": str,
|
||||
"tenant_id": str,
|
||||
"client_id": str,
|
||||
"client_secret": str,
|
||||
"allow_anonymous_login": Bool,
|
||||
"exclude_environment_credential": Bool,
|
||||
"exclude_visual_studio_code_credential": Bool,
|
||||
"exclude_shared_token_cache_credential": Bool,
|
||||
"exclude_managed_identity_credential": Bool,
|
||||
Optional("verify", default=False): Bool,
|
||||
"timeout": Coerce(int),
|
||||
"read_timeout": Coerce(int),
|
||||
"connection_timeout": Coerce(int),
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"oss": {
|
||||
"oss_key_id": str,
|
||||
"oss_key_secret": str,
|
||||
"oss_endpoint": str,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gdrive": {
|
||||
"profile": str,
|
||||
"gdrive_use_service_account": Bool,
|
||||
"gdrive_client_id": str,
|
||||
"gdrive_client_secret": str,
|
||||
"gdrive_user_credentials_file": str,
|
||||
"gdrive_service_account_user_email": str,
|
||||
"gdrive_service_account_json_file_path": str,
|
||||
Optional("gdrive_trash_only", default=False): Bool,
|
||||
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"http": HTTP_COMMON | REMOTE_COMMON,
|
||||
"https": HTTP_COMMON | REMOTE_COMMON,
|
||||
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"remote": {str: object}, # Any of the above options are valid
|
||||
}
|
||||
)
|
||||
str: ByUrl(REMOTE_SCHEMAS),
|
||||
},
|
||||
"state": {
|
||||
"dir": str, # obsoleted
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
Marker("dir", description=DEPRECATED): str, # obsoleted
|
||||
Marker("row_limit", description=DEPRECATED): All(
|
||||
Coerce(int), Range(1)
|
||||
), # obsoleted
|
||||
Marker("row_cleanup_quota", description=DEPRECATED): All(
|
||||
Coerce(int), Range(0, 100)
|
||||
), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str, # obsoleted
|
||||
Marker("dir", description=DEPRECATED): str, # obsoleted
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
@@ -336,13 +353,13 @@ SCHEMA = {
|
||||
"out_dir": str,
|
||||
},
|
||||
"exp": {
|
||||
"code": str,
|
||||
"data": str,
|
||||
"models": str,
|
||||
"metrics": str,
|
||||
"params": str,
|
||||
"plots": str,
|
||||
"live": str,
|
||||
Marker("code", description=DEPRECATED): str,
|
||||
Marker("data", description=DEPRECATED): str,
|
||||
Marker("models", description=DEPRECATED): str,
|
||||
Marker("metrics", description=DEPRECATED): str,
|
||||
Marker("params", description=DEPRECATED): str,
|
||||
Marker("plots", description=DEPRECATED): str,
|
||||
Marker("live", description=DEPRECATED): str,
|
||||
"auto_push": Bool,
|
||||
"git_remote": str,
|
||||
},
|
||||
@@ -371,3 +388,37 @@ SCHEMA = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def config_vars_for_completion(d: dict = SCHEMA, path: str = "") -> "Iterator[str]":
|
||||
for k, v in d.items():
|
||||
if k in ("machine", "feature"):
|
||||
continue
|
||||
if isinstance(k, Marker):
|
||||
if k.description == DEPRECATED:
|
||||
continue
|
||||
k = k.schema
|
||||
if not isinstance(k, str):
|
||||
continue
|
||||
keypath = f"{path}.{k}" if path else k
|
||||
if isinstance(v, dict):
|
||||
yield from config_vars_for_completion(v, keypath)
|
||||
else:
|
||||
yield keypath
|
||||
|
||||
|
||||
def contextual_config_vars_for_completion(config) -> "Iterator[str]":
|
||||
yield from config_vars_for_completion()
|
||||
for name, data in config["remote"].items():
|
||||
if "url" not in data:
|
||||
continue
|
||||
scheme = _get_schema_from_url(data["url"])
|
||||
if scheme not in REMOTE_SCHEMAS:
|
||||
continue
|
||||
schema = REMOTE_SCHEMAS[scheme]
|
||||
if schema:
|
||||
yield from config_vars_for_completion(schema, f"remote.{name}") # type: ignore[arg-type]
|
||||
|
||||
db_schema = SCHEMA["db"][str] # type: ignore[index]
|
||||
for name in config["db"]:
|
||||
yield from config_vars_for_completion(db_schema, f"db.{name}") # type: ignore[arg-type]
|
||||
|
||||
+4
-4
@@ -32,10 +32,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
||||
assert os.name == "nt"
|
||||
|
||||
from subprocess import ( # type: ignore[attr-defined]
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
STARTUPINFO,
|
||||
CREATE_NEW_PROCESS_GROUP, # ty: ignore[unresolved-import]
|
||||
CREATE_NO_WINDOW, # ty: ignore[unresolved-import]
|
||||
STARTF_USESHOWWINDOW, # ty: ignore[unresolved-import]
|
||||
STARTUPINFO, # ty: ignore[unresolved-import]
|
||||
)
|
||||
|
||||
# https://stackoverflow.com/a/7006424
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@ def _build_sugiyama_layout(vertices, edges):
|
||||
|
||||
sug.yspace = VertexViewer.HEIGHT
|
||||
sug.xspace = minw
|
||||
sug.route_edge = route_with_lines
|
||||
sug.route_edge = route_with_lines # ty: ignore[unresolved-attribute]
|
||||
|
||||
sug.draw()
|
||||
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class Client:
|
||||
except Exception as exc:
|
||||
if callable(onerror):
|
||||
onerror()
|
||||
logger.exception(
|
||||
logger.exception( # noqa: LOG007
|
||||
"Could not connect to the database. "
|
||||
"Check your database credentials and try again.",
|
||||
exc_info=False,
|
||||
|
||||
+63
-28
@@ -162,6 +162,9 @@ class FileMixin:
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def dump_stages(self, stages, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -198,6 +201,13 @@ class SingleStageFile(FileMixin):
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump_stages(self, stages, **kwargs) -> None:
|
||||
if not stages:
|
||||
return None
|
||||
|
||||
assert len(stages) == 1, "SingleStageFile can only dump one stage."
|
||||
return self.dump(stages[0], **kwargs)
|
||||
|
||||
def remove_stage(self, stage): # noqa: ARG002
|
||||
self.remove()
|
||||
|
||||
@@ -228,17 +238,27 @@ class ProjectFile(FileMixin):
|
||||
|
||||
def dump(self, stage, update_pipeline=True, update_lock=True, **kwargs):
|
||||
"""Dumps given stage appropriately in the dvcfile."""
|
||||
return self.dump_stages(
|
||||
[stage], update_pipeline=update_pipeline, update_lock=update_lock, **kwargs
|
||||
)
|
||||
|
||||
def dump_stages(self, stages, update_pipeline=True, update_lock=True, **kwargs):
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
assert isinstance(stage, PipelineStage)
|
||||
if not stages:
|
||||
return
|
||||
|
||||
for stage in stages:
|
||||
assert isinstance(stage, PipelineStage)
|
||||
|
||||
if self.verify:
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
|
||||
if update_pipeline and not stage.is_data_source:
|
||||
self._dump_pipeline_file(stage)
|
||||
if update_pipeline:
|
||||
self._dump_pipeline_file(stages)
|
||||
|
||||
if update_lock:
|
||||
self._dump_lockfile(stage, **kwargs)
|
||||
self._dump_lockfile(stages, **kwargs)
|
||||
|
||||
def dump_dataset(self, dataset):
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
@@ -260,32 +280,37 @@ class ProjectFile(FileMixin):
|
||||
raw.append(dataset)
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def _dump_lockfile(self, stage, **kwargs):
|
||||
self._lockfile.dump(stage, **kwargs)
|
||||
def _dump_lockfile(self, stages, **kwargs):
|
||||
self._lockfile.dump_stages(stages, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||
if stage.raw_data.parametrized:
|
||||
raise ParametrizedDumpError(f"cannot {action} a parametrized {stage}")
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
self._check_if_parametrized(stage)
|
||||
stage_data = serialize.to_pipeline_file(stage)
|
||||
def _dump_pipeline_file(self, stages):
|
||||
stages = stages if isinstance(stages, list) else [stages]
|
||||
if not stages:
|
||||
return
|
||||
|
||||
for stage in stages:
|
||||
self._check_if_parametrized(stage)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
if not data:
|
||||
logger.info("Creating '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
existing_entry = stage.name in data["stages"]
|
||||
action = "Modifying" if existing_entry else "Adding"
|
||||
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
||||
|
||||
if existing_entry:
|
||||
orig_stage_data = data["stages"][stage.name]
|
||||
apply_diff(stage_data[stage.name], orig_stage_data)
|
||||
else:
|
||||
data["stages"].update(stage_data)
|
||||
for stage in stages:
|
||||
stage_data = serialize.to_pipeline_file(stage)
|
||||
existing_entry = stage.name in data["stages"]
|
||||
action = "Modifying" if existing_entry else "Adding"
|
||||
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
||||
if existing_entry:
|
||||
orig_stage_data = data["stages"][stage.name]
|
||||
apply_diff(stage_data[stage.name], orig_stage_data)
|
||||
else:
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
@@ -399,9 +424,12 @@ class Lockfile(FileMixin):
|
||||
data.setdefault("stages", {})
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
def dump_stages(self, stages, **kwargs):
|
||||
if not stages:
|
||||
return
|
||||
|
||||
is_modified = False
|
||||
log_updated = False
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
if not data:
|
||||
data.update({"schema": "2.0"})
|
||||
@@ -409,17 +437,24 @@ class Lockfile(FileMixin):
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
stage.name, {}
|
||||
)
|
||||
if modified:
|
||||
logger.info("Updating lock file '%s'", self.relpath)
|
||||
for stage in stages:
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
stage.name, {}
|
||||
)
|
||||
if modified:
|
||||
is_modified = True
|
||||
if not log_updated:
|
||||
logger.info("Updating lock file '%s'", self.relpath)
|
||||
log_updated = True
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
if modified:
|
||||
if is_modified:
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
self.dump_stages([stage], **kwargs)
|
||||
|
||||
def remove_stage(self, stage):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||
from dvc.stage import Stage
|
||||
|
||||
|
||||
class DvcException(Exception):
|
||||
class DvcException(Exception): # noqa: N818
|
||||
"""Base class for all dvc exceptions."""
|
||||
|
||||
def __init__(self, msg, *args):
|
||||
@@ -242,11 +242,11 @@ class UploadError(FileTransferError):
|
||||
|
||||
|
||||
class CheckoutError(DvcException):
|
||||
def __init__(self, target_infos: list[str], stats: dict[str, list[str]]):
|
||||
def __init__(self, target_infos: list[str], result: dict):
|
||||
from dvc.utils import error_link
|
||||
|
||||
self.target_infos = target_infos
|
||||
self.stats = stats
|
||||
self.result = result
|
||||
targets = [str(t) for t in target_infos]
|
||||
m = (
|
||||
"Checkout failed for following targets:\n{}\nIs your "
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, BinaryIO, Optional, Union
|
||||
|
||||
import fsspec
|
||||
from fsspec.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
from fsspec.callbacks import TqdmCallback as _TqdmCallback
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils.objects import cached_property
|
||||
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||
from dvc.ui._rich_progress import RichTransferProgress
|
||||
|
||||
|
||||
class TqdmCallback(fsspec.callbacks.TqdmCallback):
|
||||
class TqdmCallback(_TqdmCallback):
|
||||
def __init__(
|
||||
self,
|
||||
size: Optional[int] = None,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Examples
|
||||
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from upath import UPath
|
||||
from upath import UPath # ty: ignore[unresolved-import]
|
||||
|
||||
|
||||
class DVCPath(UPath):
|
||||
|
||||
+213
-100
@@ -1,8 +1,9 @@
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from collections.abc import Iterable, Iterator
|
||||
from itertools import chain, groupby, takewhile
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Union, overload
|
||||
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pathspec.util import normalize_file
|
||||
@@ -13,7 +14,9 @@ from dvc.log import logger
|
||||
from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import AnyFSPath, FileSystem
|
||||
from typing_extensions import Self
|
||||
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
@@ -21,36 +24,79 @@ logger = logger.getChild(__name__)
|
||||
class DvcIgnore:
|
||||
DVCIGNORE_FILE = ".dvcignore"
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DvcIgnorePatterns(DvcIgnore):
|
||||
def __init__(self, pattern_list, dirname, sep):
|
||||
def __init__(
|
||||
self, pattern_list: Iterable[Union[PatternInfo, str]], dirname: str, sep: str
|
||||
) -> None:
|
||||
from pathspec.patterns.gitwildmatch import _DIR_MARK
|
||||
|
||||
if pattern_list and isinstance(pattern_list[0], str):
|
||||
pattern_list = [PatternInfo(pattern, "") for pattern in pattern_list]
|
||||
|
||||
self.sep = sep
|
||||
self.pattern_list = pattern_list
|
||||
self.dirname = dirname
|
||||
|
||||
self.regex_pattern_list = []
|
||||
for count, pattern in enumerate(pattern_list):
|
||||
pattern, group = GitWildMatchPattern.pattern_to_regex(pattern.patterns)
|
||||
if pattern:
|
||||
pattern = pattern.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
||||
self.regex_pattern_list.append((pattern, group))
|
||||
|
||||
self.ignore_spec = [
|
||||
(ignore, re.compile("|".join(item[0] for item in group)))
|
||||
for ignore, group in groupby(self.regex_pattern_list, lambda x: x[1])
|
||||
if ignore is not None
|
||||
pattern_infos = [
|
||||
pattern if isinstance(pattern, PatternInfo) else PatternInfo(pattern, "")
|
||||
for pattern in pattern_list
|
||||
]
|
||||
|
||||
self.sep = sep
|
||||
self.pattern_list: list[PatternInfo] = []
|
||||
self.dirname = dirname
|
||||
self.find_matching_pattern = functools.cache(self._find_matching_pattern)
|
||||
|
||||
regex_pattern_list: list[tuple[str, bool, bool, PatternInfo]] = []
|
||||
for count, pattern_info in enumerate(pattern_infos):
|
||||
regex, ignore = GitWildMatchPattern.pattern_to_regex(pattern_info.patterns)
|
||||
if regex is not None and ignore is not None:
|
||||
self.pattern_list.append(pattern_info)
|
||||
regex = regex.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
||||
regex_pattern_list.append(
|
||||
(regex, ignore, pattern_info.patterns.endswith("/"), pattern_info)
|
||||
)
|
||||
|
||||
def keyfunc(item: tuple[str, bool, bool, PatternInfo]) -> tuple[bool, bool]:
|
||||
_, ignore, dir_only_pattern, _ = item
|
||||
return ignore, dir_only_pattern
|
||||
|
||||
self.ignore_spec: list[
|
||||
tuple[
|
||||
re.Pattern[str],
|
||||
bool,
|
||||
bool,
|
||||
dict[Optional[str], tuple[str, PatternInfo]],
|
||||
]
|
||||
]
|
||||
self.ignore_spec = []
|
||||
for (ignore, dir_only_pattern), group in groupby(
|
||||
regex_pattern_list, key=keyfunc
|
||||
):
|
||||
if ignore:
|
||||
# For performance, we combine all exclude patterns.
|
||||
# But we still need to figure out which pattern matched which rule,
|
||||
# (eg: to show in `dvc check-ignore`).
|
||||
# So, we use named groups and keep a map of group name to pattern.
|
||||
pattern_map: dict[Optional[str], tuple[str, PatternInfo]] = {
|
||||
f"rule_{i}": (regex, pi)
|
||||
for i, (regex, _, _, pi) in enumerate(group)
|
||||
}
|
||||
combined_regex = "|".join(
|
||||
f"(?P<{name}>{regex})" for name, (regex, _) in pattern_map.items()
|
||||
)
|
||||
self.ignore_spec.append(
|
||||
(re.compile(combined_regex), ignore, dir_only_pattern, pattern_map)
|
||||
)
|
||||
else:
|
||||
# unignored patterns are not combined with `|`.
|
||||
for regex, _, _, pi in group:
|
||||
pattern_map = {None: (regex, pi)}
|
||||
self.ignore_spec.append(
|
||||
(re.compile(regex), ignore, dir_only_pattern, pattern_map)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path, fs, name):
|
||||
def from_file(cls, path: str, fs: "FileSystem", name: str) -> "Self":
|
||||
assert fs.isabs(path)
|
||||
dirname = fs.normpath(fs.dirname(path))
|
||||
with fs.open(path, encoding="utf-8") as fobj:
|
||||
@@ -62,13 +108,15 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
|
||||
return cls(path_spec_lines, dirname, fs.sep)
|
||||
|
||||
def __call__(self, root: list[str], dirs: list[str], files: list[str]):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
files = [f for f in files if not self.matches(root, f)]
|
||||
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
||||
|
||||
return dirs, files
|
||||
|
||||
def _get_normalize_path(self, dirname, basename):
|
||||
def _get_normalize_path(self, dirname: str, basename: str) -> Optional[str]:
|
||||
# NOTE: `relpath` is too slow, so we have to assume that both
|
||||
# `dirname` and `self.dirname` are relative or absolute together.
|
||||
|
||||
@@ -81,62 +129,105 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
# NOTE: `os.path.join` is ~x5.5 slower
|
||||
path = f"{rel}{self.sep}{basename}"
|
||||
else:
|
||||
return False
|
||||
return None
|
||||
|
||||
if os.name == "nt":
|
||||
return normalize_file(path)
|
||||
return path
|
||||
|
||||
def matches(self, dirname, basename, is_dir=False, details: bool = False):
|
||||
@overload
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: Literal[False] = ...,
|
||||
) -> bool: ...
|
||||
|
||||
@overload
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: Literal[True] = ...,
|
||||
) -> tuple[bool, list[PatternInfo]]: ...
|
||||
|
||||
@overload
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: bool = False,
|
||||
) -> Union[bool, tuple[bool, list[PatternInfo]]]: ...
|
||||
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: bool = False,
|
||||
) -> Union[bool, tuple[bool, list[PatternInfo]]]:
|
||||
path = self._get_normalize_path(dirname, basename)
|
||||
if not path:
|
||||
return False
|
||||
|
||||
if details:
|
||||
return self._ignore_details(path, is_dir)
|
||||
return self.ignore(path, is_dir)
|
||||
|
||||
def ignore(self, path, is_dir):
|
||||
def matches(pattern, path, is_dir) -> bool:
|
||||
matches_ = bool(pattern.match(path))
|
||||
|
||||
if is_dir:
|
||||
matches_ |= bool(pattern.match(f"{path}/"))
|
||||
|
||||
return matches_
|
||||
|
||||
result = False
|
||||
_match: list[PatternInfo] = []
|
||||
if path:
|
||||
result, _match = self._ignore(path, is_dir)
|
||||
return (result, _match) if details else result
|
||||
|
||||
for ignore, pattern in self.ignore_spec[::-1]:
|
||||
if matches(pattern, path, is_dir):
|
||||
result = ignore
|
||||
break
|
||||
return result
|
||||
def _find_matching_pattern(
|
||||
self, path: str, is_dir: bool
|
||||
) -> tuple[bool, list[PatternInfo]]:
|
||||
paths = [path]
|
||||
if is_dir and not path.endswith("/"):
|
||||
paths.append(f"{path}/")
|
||||
|
||||
def _ignore_details(self, path, is_dir: bool):
|
||||
result = []
|
||||
for (regex, _), pattern_info in list(
|
||||
zip(self.regex_pattern_list, self.pattern_list)
|
||||
for pattern, ignore, dir_only_pattern, pattern_map in reversed(
|
||||
self.ignore_spec
|
||||
):
|
||||
# skip system pattern
|
||||
if not pattern_info.file_info:
|
||||
if dir_only_pattern and not is_dir:
|
||||
continue
|
||||
for p in paths:
|
||||
match = pattern.match(p)
|
||||
if not match:
|
||||
continue
|
||||
if ignore:
|
||||
group_name, _match = next(
|
||||
(
|
||||
(name, _match)
|
||||
for name, _match in match.groupdict().items()
|
||||
if name.startswith("rule_") and _match is not None
|
||||
)
|
||||
)
|
||||
else:
|
||||
# unignored patterns are not combined with `|`,
|
||||
# so there are no groups.
|
||||
group_name = None
|
||||
_regex, pattern_info = pattern_map[group_name]
|
||||
return ignore, [pattern_info]
|
||||
return False, []
|
||||
|
||||
regex = re.compile(regex)
|
||||
def _ignore(self, path: str, is_dir: bool) -> tuple[bool, list[PatternInfo]]:
|
||||
parts = path.split("/")
|
||||
result = False
|
||||
matches: list[PatternInfo] = []
|
||||
for i in range(1, len(parts) + 1):
|
||||
rel_path = "/".join(parts[:i])
|
||||
result, _matches = self.find_matching_pattern(
|
||||
rel_path, is_dir or i < len(parts)
|
||||
)
|
||||
if i < len(parts) and not result:
|
||||
continue
|
||||
matches.extend(_matches)
|
||||
if result:
|
||||
break
|
||||
return result, matches
|
||||
|
||||
matches = bool(regex.match(path))
|
||||
if is_dir:
|
||||
matches |= bool(regex.match(f"{path}/"))
|
||||
|
||||
if matches:
|
||||
result.append(pattern_info.file_info)
|
||||
|
||||
return result
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.dirname + ":" + str(self.pattern_list))
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, DvcIgnorePatterns):
|
||||
return NotImplemented
|
||||
return (self.dirname == other.dirname) & (
|
||||
@@ -144,21 +235,18 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
== [pattern.patterns for pattern in other.pattern_list]
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.pattern_list)
|
||||
|
||||
|
||||
CheckIgnoreResult = namedtuple( # noqa: PYI024
|
||||
"CheckIgnoreResult", ["file", "match", "patterns"]
|
||||
)
|
||||
|
||||
|
||||
def _no_match(path):
|
||||
return CheckIgnoreResult(path, False, ["::"])
|
||||
class CheckIgnoreResult(NamedTuple):
|
||||
file: str
|
||||
match: bool
|
||||
pattern_infos: list[PatternInfo]
|
||||
|
||||
|
||||
class DvcIgnoreFilter:
|
||||
def __init__(self, fs, root_dir):
|
||||
def __init__(self, fs: "FileSystem", root_dir: str) -> None:
|
||||
from dvc.repo import Repo
|
||||
|
||||
default_ignore_patterns = [
|
||||
@@ -193,7 +281,7 @@ class DvcIgnoreFilter:
|
||||
ignore_subrepos=True,
|
||||
)
|
||||
|
||||
def _get_key(self, path):
|
||||
def _get_key(self, path: str) -> tuple[str, ...]:
|
||||
parts = self.fs.relparts(path, self.root_dir)
|
||||
if parts == (os.curdir,):
|
||||
return ()
|
||||
@@ -241,7 +329,7 @@ class DvcIgnoreFilter:
|
||||
for dname in dnames:
|
||||
self._update_sub_repo(self.fs.join(dirname, dname), ignore_trie)
|
||||
|
||||
def _update_sub_repo(self, path, ignore_trie: Trie):
|
||||
def _update_sub_repo(self, path: str, ignore_trie: Trie) -> None:
|
||||
from dvc.repo import Repo
|
||||
|
||||
if path == self.root_dir:
|
||||
@@ -268,7 +356,9 @@ class DvcIgnoreFilter:
|
||||
else:
|
||||
ignore_trie[key] = new_pattern
|
||||
|
||||
def __call__(self, root, dirs, files, ignore_subrepos=True):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str], ignore_subrepos: bool = True
|
||||
) -> tuple[list[str], list[str]]:
|
||||
abs_root = self.fs.abspath(root)
|
||||
ignore_pattern = self._get_trie_pattern(
|
||||
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
|
||||
@@ -277,7 +367,24 @@ class DvcIgnoreFilter:
|
||||
dirs, files = ignore_pattern(abs_root, dirs, files)
|
||||
return dirs, files
|
||||
|
||||
def ls(self, fs, path, detail=True, **kwargs):
|
||||
@overload
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: Literal[True], **kwargs: Any
|
||||
) -> list[dict[str, Any]]: ...
|
||||
|
||||
@overload
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: Literal[False], **kwargs
|
||||
) -> list[str]: ...
|
||||
|
||||
@overload
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs
|
||||
) -> Union[list[str], list[dict[str, Any]]]: ...
|
||||
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs: Any
|
||||
) -> Union[list[str], list[dict[str, Any]]]:
|
||||
fs_dict = {}
|
||||
dirs = []
|
||||
nondirs = []
|
||||
@@ -297,22 +404,29 @@ class DvcIgnoreFilter:
|
||||
|
||||
return [fs_dict[name] for name in chain(dirs, nondirs)]
|
||||
|
||||
def walk(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
|
||||
def walk(
|
||||
self, fs: "FileSystem", path: str, **kwargs: Any
|
||||
) -> Iterator[
|
||||
Union[
|
||||
tuple[str, list[str], list[str]],
|
||||
tuple[str, dict[str, dict], dict[str, dict]],
|
||||
]
|
||||
]:
|
||||
detail = kwargs.get("detail", False)
|
||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||
if fs.protocol == Schemes.LOCAL:
|
||||
for root, dirs, files in fs.walk(path, **kwargs):
|
||||
if detail:
|
||||
all_dnames = set(dirs.keys())
|
||||
all_fnames = set(files.keys())
|
||||
assert isinstance(dirs, dict)
|
||||
assert isinstance(files, dict)
|
||||
dnames, fnames = self(
|
||||
root,
|
||||
all_dnames,
|
||||
all_fnames,
|
||||
list(dirs),
|
||||
list(files),
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
)
|
||||
list(map(dirs.pop, all_dnames - set(dnames)))
|
||||
list(map(files.pop, all_fnames - set(fnames)))
|
||||
list(map(dirs.pop, dirs.keys() - set(dnames)))
|
||||
list(map(files.pop, files.keys() - set(fnames)))
|
||||
else:
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
@@ -321,7 +435,7 @@ class DvcIgnoreFilter:
|
||||
else:
|
||||
yield from fs.walk(path, **kwargs)
|
||||
|
||||
def find(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
|
||||
def find(self, fs: "FileSystem", path: str, **kwargs: Any) -> Iterator[str]:
|
||||
if fs.protocol == Schemes.LOCAL:
|
||||
for root, _, files in self.walk(fs, path, **kwargs):
|
||||
for file in files:
|
||||
@@ -331,7 +445,7 @@ class DvcIgnoreFilter:
|
||||
yield from fs.find(path)
|
||||
|
||||
def _get_trie_pattern(
|
||||
self, dirname, dnames: Optional["list"] = None, ignore_subrepos=True
|
||||
self, dirname: str, dnames: Optional[list[str]] = None, ignore_subrepos=True
|
||||
) -> Optional["DvcIgnorePatterns"]:
|
||||
if ignore_subrepos:
|
||||
ignores_trie = self.ignores_trie_fs
|
||||
@@ -367,7 +481,7 @@ class DvcIgnoreFilter:
|
||||
|
||||
def _is_ignored(
|
||||
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
|
||||
):
|
||||
) -> bool:
|
||||
if self._outside_repo(path):
|
||||
return False
|
||||
dirname, basename = self.fs.split(self.fs.normpath(path))
|
||||
@@ -389,24 +503,23 @@ class DvcIgnoreFilter:
|
||||
path = self.fs.abspath(path)
|
||||
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _outside_repo(self, path):
|
||||
def _outside_repo(self, path: str) -> bool:
|
||||
return not self.fs.isin_or_eq(path, self.root_dir)
|
||||
|
||||
def check_ignore(self, target):
|
||||
def check_ignore(self, target: str) -> CheckIgnoreResult:
|
||||
# NOTE: can only be used in `dvc check-ignore`, see
|
||||
# https://github.com/iterative/dvc/issues/5046
|
||||
full_target = self.fs.abspath(target)
|
||||
matched_patterns: list[PatternInfo] = []
|
||||
ignore = False
|
||||
if not self._outside_repo(full_target):
|
||||
dirname, basename = self.fs.split(self.fs.normpath(full_target))
|
||||
pattern = self._get_trie_pattern(dirname)
|
||||
if pattern:
|
||||
matches = pattern.matches(
|
||||
dirname, basename, self.fs.isdir(full_target), True
|
||||
ignore, matched_patterns = pattern.matches(
|
||||
dirname, basename, self.fs.isdir(full_target), details=True
|
||||
)
|
||||
|
||||
if matches:
|
||||
return CheckIgnoreResult(target, True, matches)
|
||||
return _no_match(target)
|
||||
return CheckIgnoreResult(target, ignore, matched_patterns)
|
||||
|
||||
def is_ignored(
|
||||
self, fs: "FileSystem", path: str, ignore_subrepos: bool = True
|
||||
@@ -424,7 +537,7 @@ class DvcIgnoreFilter:
|
||||
)
|
||||
|
||||
|
||||
def init(path):
|
||||
def init(path: Union[str, os.PathLike[str]]) -> str:
|
||||
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
||||
if os.path.exists(dvcignore):
|
||||
return dvcignore
|
||||
@@ -439,7 +552,7 @@ def init(path):
|
||||
return dvcignore
|
||||
|
||||
|
||||
def destroy(path):
|
||||
def destroy(path: Union[str, os.PathLike[str]]) -> None:
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
||||
|
||||
+6
-5
@@ -48,7 +48,10 @@ def get_dvc_info():
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
|
||||
info.append(f"Caches: {_get_caches(repo.cache)}")
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
configured_remotes = _get_remotes(repo.config)
|
||||
remotes = ", ".join(configured_remotes) if configured_remotes else None
|
||||
info.append(f"Remotes: {remotes}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||
@@ -75,12 +78,10 @@ def _get_caches(cache):
|
||||
|
||||
|
||||
def _get_remotes(config):
|
||||
schemes = (
|
||||
return [
|
||||
get_fs_cls(get_fs_config(config, name=remote)).protocol
|
||||
for remote in config["remote"]
|
||||
)
|
||||
|
||||
return ", ".join(schemes) or "None"
|
||||
]
|
||||
|
||||
|
||||
def _get_linktype_support_info(repo):
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
|
||||
if self._tmp_dir is not None:
|
||||
# Under Windows file path length is limited so we hash it
|
||||
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
|
||||
hasher = hashlib.md5(self._claimfile.encode(), usedforsecurity=False)
|
||||
filename = hasher.hexdigest()
|
||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||
|
||||
|
||||
+15
-3
@@ -5,6 +5,8 @@ import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import ClassVar
|
||||
|
||||
import colorama
|
||||
@@ -53,7 +55,7 @@ def add_logging_level(level_name, level_num, method_name=None):
|
||||
setattr(logging, method_name, log_to_root)
|
||||
|
||||
|
||||
class LoggingException(Exception):
|
||||
class LoggingException(Exception): # noqa: N818
|
||||
def __init__(self, record):
|
||||
msg = f"failed to log {record!s}"
|
||||
super().__init__(msg)
|
||||
@@ -174,9 +176,19 @@ def _iter_causes(exc):
|
||||
exc = exc.__cause__
|
||||
|
||||
|
||||
def set_loggers_level(level: int = logging.INFO) -> None:
|
||||
@contextmanager
|
||||
def set_loggers_level(level: int = logging.INFO) -> Iterator[None]:
|
||||
ret: dict[logging.Logger, int] = {}
|
||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||
logging.getLogger(name).setLevel(level)
|
||||
_logger = logging.getLogger(name)
|
||||
ret[_logger] = _logger.getEffectiveLevel()
|
||||
_logger.setLevel(level)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for _logger, old_lvl in ret.items():
|
||||
_logger.setLevel(old_lvl)
|
||||
|
||||
|
||||
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
||||
|
||||
+37
-17
@@ -45,7 +45,7 @@ if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
|
||||
from .ignore import DvcIgnoreFilter
|
||||
from .ignore import CheckIgnoreResult, DvcIgnoreFilter
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
@@ -267,9 +267,9 @@ class OutputIsStageFileError(DvcException):
|
||||
|
||||
|
||||
class OutputIsIgnoredError(DvcException):
|
||||
def __init__(self, match):
|
||||
lines = "\n".join(match.patterns)
|
||||
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
|
||||
def __init__(self, result: "CheckIgnoreResult"):
|
||||
pi = result.pattern_infos[-1]
|
||||
super().__init__(f"Path {result.file!r} is ignored by {pi!s}")
|
||||
|
||||
|
||||
class CheckoutCallback(TqdmCallback):
|
||||
@@ -1203,8 +1203,9 @@ class Output:
|
||||
if stage:
|
||||
abs_path = os.path.join(stage.wdir, path)
|
||||
if self._is_path_dvcignore(abs_path):
|
||||
check = stage.repo.dvcignore.check_ignore(abs_path)
|
||||
raise self.IsIgnoredError(check)
|
||||
check: CheckIgnoreResult = stage.repo.dvcignore.check_ignore(abs_path)
|
||||
if check.match:
|
||||
raise self.IsIgnoredError(check)
|
||||
|
||||
def _check_can_merge(self, out):
|
||||
if self.protocol != out.protocol:
|
||||
@@ -1452,25 +1453,44 @@ class Output:
|
||||
self.remote = other.remote
|
||||
self.can_push = other.can_push
|
||||
|
||||
def merge_version_meta(self, other: "Output"):
|
||||
def _get_versioned_meta(
|
||||
self,
|
||||
) -> Optional[
|
||||
tuple["HashInfo", Optional["Meta"], Optional[Union["HashFile", "Tree"]]]
|
||||
]:
|
||||
if self.files is not None or (
|
||||
self.meta is not None and self.meta.version_id is not None
|
||||
):
|
||||
old_obj = self.obj if self.obj is not None else self.get_obj()
|
||||
return self.hash_info, self.meta, old_obj
|
||||
return None
|
||||
|
||||
def merge_version_meta(
|
||||
self,
|
||||
old_hi: "HashInfo",
|
||||
old_meta: Optional["Meta"],
|
||||
old_obj: Optional[Union["HashFile", "Tree"]],
|
||||
):
|
||||
"""Merge version meta for files which are unchanged from other."""
|
||||
if not self.hash_info:
|
||||
return
|
||||
return None
|
||||
if self.hash_info.isdir:
|
||||
return self._merge_dir_version_meta(other)
|
||||
if self.hash_info != other.hash_info:
|
||||
return
|
||||
self.meta = other.meta
|
||||
return self._merge_dir_version_meta(old_hi, old_obj)
|
||||
if self.hash_info != old_hi:
|
||||
return None
|
||||
self.meta = old_meta
|
||||
return None
|
||||
|
||||
def _merge_dir_version_meta(self, other: "Output"):
|
||||
def _merge_dir_version_meta(
|
||||
self, old_hi: "HashInfo", old_obj: Optional[Union["HashFile", "Tree"]]
|
||||
):
|
||||
from dvc_data.hashfile.tree import update_meta
|
||||
|
||||
if not self.obj or not other.hash_info.isdir:
|
||||
if not self.obj or not old_hi.isdir:
|
||||
return
|
||||
other_obj = other.obj if other.obj is not None else other.get_obj()
|
||||
assert isinstance(self.obj, Tree)
|
||||
assert isinstance(other_obj, Tree)
|
||||
updated = update_meta(self.obj, other_obj)
|
||||
assert isinstance(old_obj, Tree)
|
||||
updated = update_meta(self.obj, old_obj)
|
||||
assert updated.hash_info == self.obj.hash_info
|
||||
self.obj = updated
|
||||
self.files = updated.as_list(with_meta=True)
|
||||
|
||||
@@ -66,7 +66,7 @@ class ParamsLoadError(ContextError):
|
||||
pass
|
||||
|
||||
|
||||
class KeyNotInContext(ContextError, KeyError):
|
||||
class KeyNotInContext(ContextError, KeyError): # noqa: N818
|
||||
def __init__(self, key: str) -> None:
|
||||
self.key: str = key
|
||||
super().__init__(f"Could not find '{key}'")
|
||||
@@ -75,7 +75,7 @@ class KeyNotInContext(ContextError, KeyError):
|
||||
return self.msg
|
||||
|
||||
|
||||
class VarsAlreadyLoaded(ContextError):
|
||||
class VarsAlreadyLoaded(ContextError): # noqa: N818
|
||||
pass
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ class CtxDict(Container, MutableMapping):
|
||||
if not isinstance(key, str):
|
||||
# limitation for the interpolation
|
||||
# ignore other kinds of keys
|
||||
return
|
||||
return None
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def merge_update(self, other, overwrite=False):
|
||||
|
||||
@@ -202,6 +202,7 @@ def validate_value(value, key):
|
||||
if isinstance(value, dict) and key == "cmd":
|
||||
return True
|
||||
raise ParseError(f"Cannot interpolate data of type '{type(value).__name__}'")
|
||||
return None
|
||||
|
||||
|
||||
def str_interpolate(
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
# Including changing base dir of path specification patterns and merging
|
||||
# of two path specification patterns with different base
|
||||
# All the operations follow the documents of `gitignore`
|
||||
from collections import namedtuple
|
||||
from typing import NamedTuple
|
||||
|
||||
from pathspec.util import normalize_file
|
||||
|
||||
from dvc.utils import relpath
|
||||
|
||||
PatternInfo = namedtuple("PatternInfo", ["patterns", "file_info"]) # noqa: PYI024
|
||||
|
||||
class PatternInfo(NamedTuple):
|
||||
patterns: str
|
||||
file_info: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.file_info or f":{self.patterns}"
|
||||
|
||||
|
||||
def _not_ignore(rule):
|
||||
|
||||
@@ -167,7 +167,7 @@ class VegaConverter(Converter):
|
||||
if isinstance(y, list):
|
||||
return "y"
|
||||
if not isinstance(y, dict):
|
||||
return
|
||||
return None
|
||||
|
||||
fields = {field for _, field in _file_field(y)}
|
||||
if len(fields) == 1:
|
||||
|
||||
@@ -641,10 +641,11 @@ class Repo:
|
||||
# that just happened to be at the same path as old deleted ones.
|
||||
btime = self._btime or getattr(os.stat(root_dir), "st_birthtime", None)
|
||||
|
||||
md5 = hashlib.md5( # noqa: S324
|
||||
md5 = hashlib.md5(
|
||||
str(
|
||||
(root_dir, subdir, btime, getpass.getuser(), version_tuple[0], salt)
|
||||
).encode()
|
||||
).encode(),
|
||||
usedforsecurity=False,
|
||||
)
|
||||
repo_token = md5.hexdigest()
|
||||
return os.path.join(repos_dir, repo_token)
|
||||
|
||||
+48
-23
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.exceptions import (
|
||||
@@ -14,7 +15,9 @@ from dvc.utils import relpath
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.index import BaseDataIndex, DataIndexEntry
|
||||
from dvc.repo.index import IndexView
|
||||
from dvc_data.index import BaseDataIndex, DataIndexEntry, DataIndexKey
|
||||
from dvc_data.index.diff import Change
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
@@ -36,10 +39,12 @@ def _remove_unused_links(repo):
|
||||
return ret
|
||||
|
||||
|
||||
def _build_out_changes(index, changes):
|
||||
def _build_out_changes(
|
||||
index: "IndexView", changes: dict["DataIndexKey", "Change"]
|
||||
) -> dict["DataIndexKey", tuple[str, dict[str, int]]]:
|
||||
from dvc_data.index.checkout import MODIFY
|
||||
|
||||
out_keys = []
|
||||
out_keys: list[DataIndexKey] = []
|
||||
for out in index.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
@@ -47,19 +52,28 @@ def _build_out_changes(index, changes):
|
||||
ws, key = out.index_key
|
||||
if ws != "repo":
|
||||
continue
|
||||
|
||||
out_keys.append(key)
|
||||
|
||||
out_changes = {}
|
||||
out_stats: dict[DataIndexKey, dict[str, int]]
|
||||
out_stats = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
out_changes: dict[DataIndexKey, tuple[str, dict[str, int]]] = {}
|
||||
for key, change in changes.items():
|
||||
typ = change.typ
|
||||
isdir = change.new and change.new.isdir
|
||||
for out_key in out_keys:
|
||||
if len(out_key) > len(key) or key[: len(out_key)] != out_key:
|
||||
continue
|
||||
|
||||
stats = out_stats[out_key]
|
||||
if not isdir:
|
||||
stats[typ] += 1
|
||||
|
||||
if key == out_key:
|
||||
out_changes[out_key] = change.typ
|
||||
elif not out_changes.get(out_key):
|
||||
out_changes[out_key] = MODIFY
|
||||
out_changes[out_key] = typ, stats
|
||||
elif out_key not in out_changes:
|
||||
typ = MODIFY
|
||||
out_changes[out_key] = typ, stats
|
||||
break
|
||||
|
||||
return out_changes
|
||||
@@ -107,14 +121,13 @@ def checkout( # noqa: C901
|
||||
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
|
||||
from dvc_data.index.checkout import ADD, DELETE, MODIFY, apply, compare
|
||||
|
||||
stats: dict[str, list[str]] = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
}
|
||||
stats = {"modified": 0, "added": 0, "deleted": 0}
|
||||
changes: dict[str, list[str]] = {"modified": [], "added": [], "deleted": []}
|
||||
|
||||
if not targets:
|
||||
targets = [None]
|
||||
stats["deleted"] = _remove_unused_links(self)
|
||||
changes["deleted"] = _remove_unused_links(self)
|
||||
stats["deleted"] = len(changes["deleted"])
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
@@ -127,8 +140,10 @@ def checkout( # noqa: C901
|
||||
raise CheckoutErrorSuggestGit(target) from exc
|
||||
raise # noqa: PLE0704
|
||||
|
||||
view = self.index.targets_view(
|
||||
targets, recursive=recursive, with_deps=with_deps, onerror=onerror
|
||||
from .index import index_from_targets
|
||||
|
||||
view = index_from_targets(
|
||||
self, targets=targets, recursive=recursive, with_deps=with_deps, onerror=onerror
|
||||
)
|
||||
|
||||
with ui.progress(unit="entry", desc="Building workspace index", leave=True) as pb:
|
||||
@@ -174,16 +189,26 @@ def checkout( # noqa: C901
|
||||
out_changes = _build_out_changes(view, diff.changes)
|
||||
|
||||
typ_map = {ADD: "added", DELETE: "deleted", MODIFY: "modified"}
|
||||
for key, typ in out_changes.items():
|
||||
for key, (typ, _stats) in out_changes.items():
|
||||
out_path = self.fs.join(self.root_dir, *key)
|
||||
|
||||
if out_path in failed:
|
||||
self.fs.remove(out_path, recursive=True)
|
||||
else:
|
||||
self.state.save_link(out_path, self.fs)
|
||||
stats[typ_map[typ]].append(_fspath_dir(out_path))
|
||||
continue
|
||||
|
||||
self.state.save_link(out_path, self.fs)
|
||||
for t, count in _stats.items():
|
||||
stats_typ = typ_map[t]
|
||||
stats[stats_typ] += count
|
||||
|
||||
changes[typ_map[typ]].append(_fspath_dir(out_path))
|
||||
|
||||
for changelist in changes.values():
|
||||
# group directories first, then files. But keep them alphabetically sorted
|
||||
changelist.sort(key=lambda p: (not p.endswith(os.sep), p))
|
||||
|
||||
result = changes | {"stats": stats}
|
||||
if failed and not allow_missing:
|
||||
raise CheckoutError([relpath(out_path) for out_path in failed], stats)
|
||||
|
||||
return stats
|
||||
result["failed"] = [relpath(out_path) for out_path in failed]
|
||||
raise CheckoutError([relpath(out_path) for out_path in failed], result)
|
||||
return result
|
||||
|
||||
+3
-3
@@ -43,13 +43,13 @@ def _collect_paths(
|
||||
|
||||
|
||||
def _filter_outs(
|
||||
outs: Outputs, fs_paths: StrPaths, duplicates=False
|
||||
repo: "Repo", outs: Outputs, fs_paths: StrPaths, duplicates=False
|
||||
) -> tuple[Outputs, StrPaths]:
|
||||
res_outs: Outputs = []
|
||||
fs_res_paths = fs_paths
|
||||
|
||||
for out in outs:
|
||||
fs_path = out.repo.dvcfs.from_os_path(out.fs_path)
|
||||
fs_path = repo.dvcfs.from_os_path(out.fs_path)
|
||||
if fs_path in fs_paths:
|
||||
res_outs.append(out)
|
||||
if not duplicates:
|
||||
@@ -77,4 +77,4 @@ def collect(
|
||||
|
||||
target_paths = _collect_paths(repo, targets, recursive=recursive)
|
||||
|
||||
return _filter_outs(outs, target_paths, duplicates=duplicates)
|
||||
return _filter_outs(repo, outs, target_paths, duplicates=duplicates)
|
||||
|
||||
+32
-22
@@ -1,3 +1,4 @@
|
||||
from itertools import groupby
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc import prompt
|
||||
@@ -52,29 +53,38 @@ def commit(
|
||||
data_only=False,
|
||||
relink=True,
|
||||
):
|
||||
stages_info = [
|
||||
info
|
||||
for info in self.stage.collect_granular(
|
||||
target, with_deps=with_deps, recursive=recursive
|
||||
)
|
||||
if not data_only or info.stage.is_data_source
|
||||
]
|
||||
for stage_info in stages_info:
|
||||
stage = stage_info.stage
|
||||
if force:
|
||||
stage.save(allow_missing=allow_missing)
|
||||
else:
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
committed_stages = []
|
||||
groups = groupby(
|
||||
[
|
||||
info
|
||||
for info in self.stage.collect_granular(
|
||||
target, with_deps=with_deps, recursive=recursive
|
||||
)
|
||||
if not data_only or info.stage.is_data_source
|
||||
],
|
||||
key=lambda info: info.stage.dvcfile,
|
||||
)
|
||||
|
||||
for dvcfile, stages_info_group in groups:
|
||||
to_dump = []
|
||||
for stage_info in stages_info_group:
|
||||
stage = stage_info.stage
|
||||
if force:
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info,
|
||||
allow_missing=allow_missing,
|
||||
relink=relink,
|
||||
)
|
||||
stage.dump(update_pipeline=False)
|
||||
return [s.stage for s in stages_info]
|
||||
else:
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info,
|
||||
allow_missing=allow_missing,
|
||||
relink=relink,
|
||||
)
|
||||
to_dump.append(stage)
|
||||
dvcfile.dump_stages(to_dump, update_pipeline=False)
|
||||
committed_stages.extend(to_dump)
|
||||
return committed_stages
|
||||
|
||||
|
||||
@locked
|
||||
|
||||
+130
-25
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Optional, TypedDict, Union
|
||||
from dvc.fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
|
||||
from dvc.log import logger
|
||||
from dvc.ui import ui
|
||||
from dvc_data.index import DataIndexDirError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
@@ -40,6 +41,14 @@ def _adapt_path(change: "Change") -> str:
|
||||
return os.path.sep.join(key)
|
||||
|
||||
|
||||
def _adapt_path_from_entry(entry: "DataIndexEntry") -> str:
|
||||
key = entry.key
|
||||
assert key
|
||||
if entry.meta and entry.meta.isdir:
|
||||
key = (*key, "")
|
||||
return os.sep.join(key)
|
||||
|
||||
|
||||
def _get_missing_paths(
|
||||
to_check: Mapping["FileSystem", Mapping[str, Iterable["DataIndexEntry"]]],
|
||||
batch_size: Optional[int] = None,
|
||||
@@ -58,11 +67,7 @@ def _get_missing_paths(
|
||||
continue
|
||||
|
||||
for entry in paths_map[cache_path]:
|
||||
key = entry.key
|
||||
assert key
|
||||
if entry.meta and entry.meta.isdir:
|
||||
key = (*key, "")
|
||||
yield os.path.sep.join(key)
|
||||
yield _adapt_path_from_entry(entry)
|
||||
|
||||
|
||||
class StorageCallback(Callback):
|
||||
@@ -81,6 +86,21 @@ class StorageCallback(Callback):
|
||||
self.parent_cb.relative_update(value - self.value)
|
||||
|
||||
|
||||
class Rename(TypedDict):
|
||||
old: str
|
||||
new: str
|
||||
|
||||
|
||||
class DiffResult(TypedDict, total=False):
|
||||
modified: list[str]
|
||||
added: list[str]
|
||||
deleted: list[str]
|
||||
renamed: list[Rename]
|
||||
unchanged: list[str]
|
||||
unknown: list[str]
|
||||
not_in_cache: list[str]
|
||||
|
||||
|
||||
def _diff(
|
||||
old: "BaseDataIndex",
|
||||
new: "BaseDataIndex",
|
||||
@@ -90,11 +110,25 @@ def _diff(
|
||||
not_in_cache: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
) -> dict[str, list[str]]:
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY, UNCHANGED, UNKNOWN, diff
|
||||
with_renames: bool = False,
|
||||
) -> DiffResult:
|
||||
from dvc_data.index.diff import (
|
||||
ADD,
|
||||
DELETE,
|
||||
MODIFY,
|
||||
RENAME,
|
||||
UNCHANGED,
|
||||
UNKNOWN,
|
||||
diff,
|
||||
)
|
||||
|
||||
ret: dict[str, list[str]] = defaultdict(list)
|
||||
change_types = {MODIFY: "modified", ADD: "added", DELETE: "deleted"}
|
||||
ret: DiffResult = defaultdict(list) # type: ignore[assignment]
|
||||
change_types = {
|
||||
MODIFY: "modified",
|
||||
ADD: "added",
|
||||
DELETE: "deleted",
|
||||
RENAME: "renamed",
|
||||
}
|
||||
|
||||
to_check: dict[FileSystem, dict[str, list[DataIndexEntry]]] = defaultdict(
|
||||
lambda: defaultdict(list)
|
||||
@@ -107,25 +141,38 @@ def _diff(
|
||||
shallow=not granular,
|
||||
hash_only=True,
|
||||
with_unknown=True,
|
||||
with_renames=with_renames,
|
||||
callback=callback,
|
||||
):
|
||||
typ = change.typ
|
||||
|
||||
# The index is a trie, so even when we filter by a specific path
|
||||
# like `dir/file`, all parent nodes leading to that path (e.g., `dir/`)
|
||||
# still appear in the view. As a result, keys like `dir/` will be present
|
||||
# even if only `dir/file` matches the filter.
|
||||
# We need to skip such entries to avoid showing root of tracked directories.
|
||||
if filter_keys and not any(change.key[: len(fk)] == fk for fk in filter_keys):
|
||||
continue
|
||||
if filter_keys:
|
||||
# RENAME does not have a `change.key`
|
||||
if typ == RENAME:
|
||||
assert change.new
|
||||
key = change.new.key
|
||||
# match with "new" key only
|
||||
assert key
|
||||
else:
|
||||
key = change.key
|
||||
|
||||
if not any(key[: len(fk)] == fk for fk in filter_keys):
|
||||
continue
|
||||
|
||||
if (
|
||||
change.typ == UNCHANGED
|
||||
typ == UNCHANGED
|
||||
and (not change.old or not change.old.hash_info)
|
||||
and (not change.new or not change.new.hash_info)
|
||||
):
|
||||
# NOTE: emulating previous behaviour
|
||||
continue
|
||||
|
||||
if change.typ == UNKNOWN and not change.new:
|
||||
if typ == UNKNOWN and not change.new:
|
||||
# NOTE: emulating previous behaviour
|
||||
continue
|
||||
|
||||
@@ -135,8 +182,18 @@ def _diff(
|
||||
# check later in batches
|
||||
to_check[cache_fs][cache_path].append(old_entry)
|
||||
|
||||
change_typ = change_types.get(change.typ, change.typ)
|
||||
ret[change_typ].append(_adapt_path(change))
|
||||
change_typ = change_types.get(typ, typ)
|
||||
if typ == RENAME:
|
||||
assert change.old is not None
|
||||
assert change.new is not None
|
||||
ret["renamed"].append(
|
||||
{
|
||||
"old": _adapt_path_from_entry(change.old),
|
||||
"new": _adapt_path_from_entry(change.new),
|
||||
}
|
||||
)
|
||||
else:
|
||||
ret[change_typ].append(_adapt_path(change)) # type: ignore[literal-required]
|
||||
|
||||
total_items = sum(
|
||||
len(entries) for paths in to_check.values() for entries in paths.values()
|
||||
@@ -149,7 +206,7 @@ def _diff(
|
||||
)
|
||||
if missing_items:
|
||||
ret["not_in_cache"] = missing_items
|
||||
return dict(ret)
|
||||
return dict(ret) # type: ignore[return-value]
|
||||
|
||||
|
||||
class GitInfo(TypedDict, total=False):
|
||||
@@ -229,7 +286,8 @@ def _diff_index_to_wtree(
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
granular: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
) -> dict[str, list[str]]:
|
||||
with_renames: bool = False,
|
||||
) -> DiffResult:
|
||||
from .index import build_data_index
|
||||
|
||||
with ui.progress(desc="Building workspace index", unit="entry") as pb:
|
||||
@@ -254,6 +312,7 @@ def _diff_index_to_wtree(
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
not_in_cache=True,
|
||||
with_renames=with_renames,
|
||||
batch_size=batch_size,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
@@ -264,7 +323,9 @@ def _diff_head_to_index(
|
||||
head: str = "HEAD",
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
granular: bool = False,
|
||||
) -> dict[str, list[str]]:
|
||||
with_renames: bool = False,
|
||||
) -> DiffResult:
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.scm import RevError
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
@@ -278,6 +339,11 @@ def _diff_head_to_index(
|
||||
except RevError:
|
||||
logger.debug("failed to switch to '%s'", head)
|
||||
head_view = DataIndex()
|
||||
except NotDvcRepoError as exc:
|
||||
# NOTE: this only gets raised on subdir repos at the moment,
|
||||
# which looks like a bug in `repo.switch`.
|
||||
logger.warning(exc)
|
||||
head_view = DataIndex()
|
||||
|
||||
with ui.progress(desc="Calculating diff between head/index", unit="entry") as pb:
|
||||
return _diff(
|
||||
@@ -285,6 +351,7 @@ def _diff_head_to_index(
|
||||
index_view,
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
with_renames=with_renames,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
|
||||
@@ -292,8 +359,8 @@ def _diff_head_to_index(
|
||||
class Status(TypedDict):
|
||||
not_in_cache: list[str]
|
||||
not_in_remote: list[str]
|
||||
committed: dict[str, list[str]]
|
||||
uncommitted: dict[str, list[str]]
|
||||
committed: DiffResult
|
||||
uncommitted: DiffResult
|
||||
untracked: list[str]
|
||||
unchanged: list[str]
|
||||
git: GitInfo
|
||||
@@ -328,16 +395,29 @@ def _get_entries_not_in_remote(
|
||||
from dvc.repo.worktree import worktree_view
|
||||
from dvc_data.index import StorageKeyError
|
||||
|
||||
entries: dict[DataIndexKey, DataIndexEntry] = {}
|
||||
|
||||
def _onerror(entry, exc):
|
||||
if not isinstance(exc, DataIndexDirError):
|
||||
raise exc
|
||||
# We don't have the contents of this dir file, so we will only check this key.
|
||||
entries[entry.key] = entry
|
||||
|
||||
# View into the index, with only pushable entries
|
||||
index = worktree_view(repo.index, push=True)
|
||||
data_index = index.data["repo"]
|
||||
|
||||
orig_data_index_onerror = data_index.onerror
|
||||
data_index.onerror = _onerror
|
||||
|
||||
view = filter_index(data_index, filter_keys=filter_keys) # type: ignore[arg-type]
|
||||
|
||||
missing_entries = []
|
||||
|
||||
storage_map = view.storage_map
|
||||
with TqdmCallback(size=0, desc="Checking remote", unit="entry") as cb:
|
||||
|
||||
n = 0
|
||||
with TqdmCallback(size=n, desc="Checking remote", unit="entry") as cb:
|
||||
for key, entry in view.iteritems(shallow=not granular):
|
||||
if not (entry and entry.hash_info):
|
||||
continue
|
||||
@@ -352,13 +432,20 @@ def _get_entries_not_in_remote(
|
||||
):
|
||||
continue
|
||||
|
||||
entries[key] = entry
|
||||
n += 1
|
||||
cb.set_size(n)
|
||||
|
||||
for key, entry in entries.items():
|
||||
k = (*key, "") if entry.meta and entry.meta.isdir else key
|
||||
try:
|
||||
if not storage_map.remote_exists(entry, refresh=remote_refresh):
|
||||
missing_entries.append(os.path.sep.join(k))
|
||||
cb.relative_update() # no need to update the size
|
||||
except StorageKeyError:
|
||||
pass
|
||||
finally:
|
||||
cb.relative_update()
|
||||
data_index.onerror = orig_data_index_onerror
|
||||
return missing_entries
|
||||
|
||||
|
||||
@@ -377,26 +464,40 @@ def _prune_keys(filter_keys: Iterable["DataIndexKey"]) -> list["DataIndexKey"]:
|
||||
return result
|
||||
|
||||
|
||||
def status(
|
||||
def status( # noqa: PLR0913
|
||||
repo: "Repo",
|
||||
targets: Optional[Iterable[Union[os.PathLike[str], str]]] = None,
|
||||
*,
|
||||
granular: bool = False,
|
||||
untracked_files: str = "no",
|
||||
remote: Optional[str] = None,
|
||||
not_in_remote: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
config: Optional[dict] = None,
|
||||
batch_size: Optional[int] = None,
|
||||
head: str = "HEAD",
|
||||
with_renames: bool = True,
|
||||
) -> Status:
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
config = config or {}
|
||||
if remote and not_in_remote:
|
||||
logger.debug("Using remote %r", remote)
|
||||
core = config.setdefault("core", {})
|
||||
core["remote"] = remote
|
||||
repo.config.merge(config)
|
||||
|
||||
targets = targets or []
|
||||
filter_keys: list[DataIndexKey] = [repo.fs.relparts(os.fspath(t)) for t in targets]
|
||||
# try to remove duplicate and overlapping keys
|
||||
filter_keys = _prune_keys(filter_keys)
|
||||
|
||||
uncommitted_diff = _diff_index_to_wtree(
|
||||
repo, filter_keys=filter_keys, granular=granular, batch_size=batch_size
|
||||
repo,
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
batch_size=batch_size,
|
||||
with_renames=with_renames,
|
||||
)
|
||||
unchanged = set(uncommitted_diff.pop("unchanged", []))
|
||||
|
||||
@@ -411,7 +512,11 @@ def status(
|
||||
|
||||
try:
|
||||
committed_diff = _diff_head_to_index(
|
||||
repo, filter_keys=filter_keys, head=head, granular=granular
|
||||
repo,
|
||||
filter_keys=filter_keys,
|
||||
head=head,
|
||||
granular=granular,
|
||||
with_renames=with_renames,
|
||||
)
|
||||
except (SCMError, NoSCMError):
|
||||
committed_diff = {}
|
||||
|
||||
@@ -45,6 +45,12 @@ def _diff(old, new, data_keys, with_missing=False):
|
||||
with_renames=True,
|
||||
meta_cmp_key=meta_cmp_key,
|
||||
roots=data_keys,
|
||||
# Include unknown entries from missing dir entry, so that they don't
|
||||
# get reported as added/modified/deleted.
|
||||
# Also return unchanged entries so that we can check if they are missing
|
||||
# from cache.
|
||||
with_unknown=True,
|
||||
with_unchanged=with_missing,
|
||||
):
|
||||
if (change.old and change.old.isdir and not change.old.hash_info) or (
|
||||
change.new and change.new.isdir and not change.new.hash_info
|
||||
|
||||
@@ -274,6 +274,7 @@ def collect(
|
||||
num: int = 1,
|
||||
hide_queued: bool = False,
|
||||
hide_failed: bool = False,
|
||||
hide_workspace: bool = False,
|
||||
sha_only: bool = False,
|
||||
**kwargs,
|
||||
) -> list["ExpState"]:
|
||||
@@ -303,8 +304,11 @@ def collect(
|
||||
repo.scm, baseline_revs, refs=cached_refs, logger=logger
|
||||
)
|
||||
|
||||
workspace_data = collect_rev(repo, "workspace", **kwargs)
|
||||
result: list[ExpState] = [workspace_data]
|
||||
result: list[ExpState] = []
|
||||
if not hide_workspace:
|
||||
workspace_data = collect_rev(repo, "workspace", **kwargs)
|
||||
result.append(workspace_data)
|
||||
|
||||
queued = collect_queued(repo, baseline_revs, **kwargs) if not hide_queued else {}
|
||||
active = collect_active(repo, baseline_revs, **kwargs)
|
||||
failed = collect_failed(repo, baseline_revs, **kwargs) if not hide_failed else {}
|
||||
|
||||
@@ -50,7 +50,7 @@ class MultipleBranchError(DvcException):
|
||||
self.ref_infos = ref_infos
|
||||
|
||||
|
||||
class AmbiguousExpRefInfo(InvalidArgumentError):
|
||||
class AmbiguousExpRefInfo(InvalidArgumentError): # noqa: N818
|
||||
def __init__(self, exp_name: str, exp_ref_list: Iterable["ExpRefInfo"]):
|
||||
msg = [
|
||||
(
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from contextlib import AbstractContextManager, contextmanager
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import IntEnum
|
||||
from itertools import chain
|
||||
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, Union
|
||||
|
||||
from funcy import nullcontext
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
from dvc.logger import set_loggers_level
|
||||
from dvc.repo.experiments.exceptions import ExperimentExistsError
|
||||
from dvc.repo.experiments.refs import EXEC_BASELINE, EXEC_BRANCH, ExpRefInfo
|
||||
from dvc.repo.experiments.utils import to_studio_params
|
||||
@@ -297,7 +298,9 @@ class BaseExecutor(ABC):
|
||||
stages = dvc.commit([], recursive=recursive, force=True, relink=False)
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if include_untracked:
|
||||
dvc.scm.add(include_untracked, force=True) # type: ignore[call-arg]
|
||||
from dvc.scm import add_no_submodules
|
||||
|
||||
add_no_submodules(dvc.scm, include_untracked, force=True) # type: ignore[call-arg]
|
||||
|
||||
with cls.auto_push(dvc):
|
||||
cls.commit(
|
||||
@@ -466,8 +469,8 @@ class BaseExecutor(ABC):
|
||||
|
||||
if queue is not None:
|
||||
queue.put((rev, os.getpid()))
|
||||
if log_errors and log_level is not None:
|
||||
cls._set_log_level(log_level)
|
||||
|
||||
log_ctx = cls._set_log_level(log_level) if log_errors else nullcontext()
|
||||
|
||||
exp_hash: Optional[str] = None
|
||||
exp_ref: Optional[ExpRefInfo] = None
|
||||
@@ -476,14 +479,17 @@ class BaseExecutor(ABC):
|
||||
if info.name:
|
||||
ui.write(f"Reproducing experiment '{info.name}'")
|
||||
|
||||
with cls._repro_dvc(
|
||||
info,
|
||||
infofile,
|
||||
log_errors=log_errors,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
**kwargs,
|
||||
) as dvc:
|
||||
with (
|
||||
log_ctx,
|
||||
cls._repro_dvc(
|
||||
info,
|
||||
infofile,
|
||||
log_errors=log_errors,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
**kwargs,
|
||||
) as dvc,
|
||||
):
|
||||
args, kwargs = cls._repro_args(dvc)
|
||||
if args:
|
||||
targets: Optional[Union[list, str]] = args[0]
|
||||
@@ -509,7 +515,9 @@ class BaseExecutor(ABC):
|
||||
stages = dvc.reproduce(*args, **kwargs)
|
||||
if paths := cls._get_top_level_paths(dvc):
|
||||
logger.debug("Staging top-level files: %s", paths)
|
||||
dvc.scm_context.add(paths)
|
||||
from dvc.scm import add_no_submodules
|
||||
|
||||
add_no_submodules(dvc.scm, paths)
|
||||
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if not repro_dry:
|
||||
@@ -786,13 +794,10 @@ class BaseExecutor(ABC):
|
||||
return orig_rev
|
||||
|
||||
@staticmethod
|
||||
def _set_log_level(level):
|
||||
# When executor.reproduce is run in a multiprocessing child process,
|
||||
# dvc.cli.main will not be called for that child process so we need to
|
||||
# setup logging ourselves
|
||||
dvc_logger = logging.getLogger("dvc")
|
||||
def _set_log_level(level: Optional[int]) -> AbstractContextManager[None]:
|
||||
if level is not None:
|
||||
dvc_logger.setLevel(level)
|
||||
return set_loggers_level(level)
|
||||
return nullcontext()
|
||||
|
||||
@staticmethod
|
||||
def _copy_path(src, dst):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import glob
|
||||
import hashlib
|
||||
import locale
|
||||
import logging
|
||||
@@ -434,9 +435,13 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
try:
|
||||
proc_info = self.proc[queue_entry.stash_rev]
|
||||
except KeyError:
|
||||
raise DvcException( # noqa: B904
|
||||
f"No output logs found for experiment '{rev}'"
|
||||
)
|
||||
message = f"No output logs found for experiment '{rev}'"
|
||||
if self.match_queue_entry_by_name({rev}, self.iter_failed()).get(rev):
|
||||
message += "\nExperiment likely failed during setup."
|
||||
if celery_logs := glob.glob(os.path.join(self.wdir, "*.out")):
|
||||
message += " Check the celery logs for more details:"
|
||||
message += f"\n\tcat {' '.join(celery_logs)}".expandtabs(4)
|
||||
raise DvcException(message) # noqa: B904
|
||||
with open(
|
||||
proc_info.stdout, encoding=encoding or locale.getpreferredencoding()
|
||||
) as fobj:
|
||||
|
||||
@@ -30,6 +30,7 @@ def show(
|
||||
num: int = 1,
|
||||
hide_queued: bool = False,
|
||||
hide_failed: bool = False,
|
||||
hide_workspace: bool = False,
|
||||
sha_only: bool = False,
|
||||
**kwargs,
|
||||
) -> list["ExpState"]:
|
||||
@@ -42,6 +43,7 @@ def show(
|
||||
num=num,
|
||||
hide_queued=hide_queued,
|
||||
hide_failed=hide_failed,
|
||||
hide_workspace=hide_workspace,
|
||||
sha_only=sha_only,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
+8
-3
@@ -10,6 +10,7 @@ from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage import Stage
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
@@ -19,12 +20,13 @@ def _make_index_onerror(onerror, rev):
|
||||
def _onerror(entry, exc):
|
||||
if onerror:
|
||||
return onerror(rev, entry, exc)
|
||||
return None
|
||||
|
||||
return _onerror
|
||||
|
||||
|
||||
def _collect_indexes( # noqa: PLR0913
|
||||
repo,
|
||||
repo: "Repo",
|
||||
targets=None,
|
||||
remote=None,
|
||||
all_branches=False,
|
||||
@@ -40,6 +42,8 @@ def _collect_indexes( # noqa: PLR0913
|
||||
onerror=None,
|
||||
push=False,
|
||||
):
|
||||
from .index import index_from_targets
|
||||
|
||||
indexes = {}
|
||||
collection_exc = None
|
||||
|
||||
@@ -67,7 +71,8 @@ def _collect_indexes( # noqa: PLR0913
|
||||
try:
|
||||
repo.config.merge(config)
|
||||
|
||||
idx = repo.index.targets_view(
|
||||
idx = index_from_targets(
|
||||
repo,
|
||||
targets,
|
||||
with_deps=with_deps,
|
||||
recursive=recursive,
|
||||
@@ -94,7 +99,7 @@ def _collect_indexes( # noqa: PLR0913
|
||||
|
||||
@locked
|
||||
def fetch( # noqa: PLR0913
|
||||
self,
|
||||
self: "Repo",
|
||||
targets=None,
|
||||
jobs=None,
|
||||
remote=None,
|
||||
|
||||
+91
-29
@@ -17,6 +17,7 @@ from dvc.utils.objects import cached_property
|
||||
if TYPE_CHECKING:
|
||||
from networkx import DiGraph
|
||||
from pygtrie import Trie
|
||||
from typing_extensions import Self
|
||||
|
||||
from dvc.dependency import Dependency
|
||||
from dvc.fs.callbacks import Callback
|
||||
@@ -66,14 +67,14 @@ def collect_files(
|
||||
|
||||
is_local_fs = isinstance(fs, LocalFileSystem)
|
||||
|
||||
def is_ignored(path):
|
||||
def is_ignored(path: str) -> bool:
|
||||
# apply only for the local fs
|
||||
return is_local_fs and scm.is_ignored(path)
|
||||
|
||||
def is_dvcfile_and_not_ignored(root, file):
|
||||
def is_dvcfile_and_not_ignored(root: str, file: str) -> bool:
|
||||
return is_valid_filename(file) and not is_ignored(f"{root}{sep}{file}")
|
||||
|
||||
def is_out_or_ignored(root, directory):
|
||||
def is_out_or_ignored(root: str, directory: str) -> bool:
|
||||
dir_path = f"{root}{sep}{directory}"
|
||||
# trailing slash needed to check if a directory is gitignored
|
||||
return dir_path in outs or is_ignored(f"{dir_path}{sep}")
|
||||
@@ -83,6 +84,8 @@ def collect_files(
|
||||
walk_iter = log_walk(walk_iter)
|
||||
|
||||
for root, dirs, files in walk_iter:
|
||||
assert isinstance(dirs, list)
|
||||
assert isinstance(files, list)
|
||||
dvcfile_filter = partial(is_dvcfile_and_not_ignored, root)
|
||||
for file in filter(dvcfile_filter, files):
|
||||
file_path = fs.join(root, file)
|
||||
@@ -318,32 +321,9 @@ class Index:
|
||||
repo: "Repo",
|
||||
onerror: Optional[Callable[[str, Exception], None]] = None,
|
||||
) -> "Index":
|
||||
stages = []
|
||||
metrics = {}
|
||||
plots = {}
|
||||
params = {}
|
||||
artifacts = {}
|
||||
datasets = {}
|
||||
datasets_lock = {}
|
||||
|
||||
onerror = onerror or repo.stage_collection_error_handler
|
||||
for _, idx in collect_files(repo, onerror=onerror):
|
||||
stages.extend(idx.stages)
|
||||
metrics.update(idx._metrics)
|
||||
plots.update(idx._plots)
|
||||
params.update(idx._params)
|
||||
artifacts.update(idx._artifacts)
|
||||
datasets.update(idx._datasets)
|
||||
datasets_lock.update(idx._datasets_lock)
|
||||
return cls(
|
||||
repo,
|
||||
stages=stages,
|
||||
metrics=metrics,
|
||||
plots=plots,
|
||||
params=params,
|
||||
artifacts=artifacts,
|
||||
datasets=datasets,
|
||||
datasets_lock=datasets_lock,
|
||||
return cls.from_indexes(
|
||||
repo, (idx for _, idx in collect_files(repo, onerror=onerror))
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -364,7 +344,7 @@ class Index:
|
||||
else {},
|
||||
)
|
||||
|
||||
def update(self, stages: Iterable["Stage"]) -> "Index":
|
||||
def update(self, stages: Iterable["Stage"]) -> "Self":
|
||||
stages = set(stages)
|
||||
# we remove existing stages with same hashes at first
|
||||
# and then re-add the new ones later.
|
||||
@@ -379,6 +359,36 @@ class Index:
|
||||
datasets=self._datasets,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_indexes(cls, repo, idxs: Iterable["Self"]) -> "Self":
|
||||
stages = []
|
||||
metrics = {}
|
||||
plots = {}
|
||||
params = {}
|
||||
artifacts = {}
|
||||
datasets = {}
|
||||
datasets_lock = {}
|
||||
|
||||
for idx in idxs:
|
||||
stages.extend(idx.stages)
|
||||
metrics.update(idx._metrics)
|
||||
plots.update(idx._plots)
|
||||
params.update(idx._params)
|
||||
artifacts.update(idx._artifacts)
|
||||
datasets.update(idx._datasets)
|
||||
datasets_lock.update(idx._datasets_lock)
|
||||
|
||||
return cls(
|
||||
repo,
|
||||
stages=stages,
|
||||
metrics=metrics,
|
||||
plots=plots,
|
||||
params=params,
|
||||
artifacts=artifacts,
|
||||
datasets=datasets,
|
||||
datasets_lock=datasets_lock,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def outs_trie(self) -> "Trie":
|
||||
from dvc.repo.trie import build_outs_trie
|
||||
@@ -735,6 +745,10 @@ class IndexView:
|
||||
for stage in self.stages:
|
||||
yield from stage.deps
|
||||
|
||||
@property
|
||||
def index(self) -> "Index":
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def _filtered_outs(self) -> Iterator[tuple["Output", Optional[str]]]:
|
||||
for stage, filter_info in self._stage_infos:
|
||||
@@ -927,3 +941,51 @@ def _get_entry_hash_name(
|
||||
return src_entry.hash_info.name
|
||||
|
||||
return DEFAULT_ALGORITHM
|
||||
|
||||
|
||||
def index_from_targets(
|
||||
repo: "Repo",
|
||||
targets: Optional["TargetType"] = None,
|
||||
stage_filter: Optional[Callable[["Stage"], bool]] = None,
|
||||
outs_filter: Optional[Callable[["Output"], bool]] = None,
|
||||
max_size: Optional[int] = None,
|
||||
types: Optional[list[str]] = None,
|
||||
with_deps: bool = False,
|
||||
recursive: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> "IndexView":
|
||||
from dvc.stage.exceptions import StageFileDoesNotExistError, StageNotFound
|
||||
from dvc.utils import parse_target
|
||||
|
||||
index: Optional[Index] = None
|
||||
if targets and all(targets) and not with_deps and not recursive:
|
||||
indexes: list[Index] = []
|
||||
try:
|
||||
for target in targets:
|
||||
if not target:
|
||||
continue
|
||||
file, name = parse_target(target)
|
||||
if file and not name:
|
||||
index = Index.from_file(repo, file)
|
||||
else:
|
||||
stages = repo.stage.collect(target)
|
||||
index = Index(repo, stages=list(stages))
|
||||
indexes.append(index)
|
||||
except (StageFileDoesNotExistError, StageNotFound):
|
||||
pass
|
||||
else:
|
||||
index = Index.from_indexes(repo, indexes)
|
||||
targets = None
|
||||
|
||||
if index is None:
|
||||
index = repo.index
|
||||
return index.targets_view(
|
||||
targets,
|
||||
stage_filter=stage_filter,
|
||||
outs_filter=outs_filter,
|
||||
max_size=max_size,
|
||||
types=types,
|
||||
recursive=recursive,
|
||||
with_deps=with_deps,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False): # noqa:
|
||||
|
||||
remove(dvc_dir)
|
||||
|
||||
os.mkdir(dvc_dir)
|
||||
os.makedirs(dvc_dir, exist_ok=True)
|
||||
|
||||
config = Config.init(dvc_dir)
|
||||
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ def install(self: "Repo", use_pre_commit_tool: bool = False) -> None:
|
||||
|
||||
scm = self.scm
|
||||
if not isinstance(scm, Git):
|
||||
return
|
||||
return None
|
||||
|
||||
driver = "dvc git-hook merge-driver --ancestor %O --our %A --their %B "
|
||||
scm.install_merge_driver("dvc", "DVC merge driver", driver)
|
||||
|
||||
@@ -346,14 +346,14 @@ def _get_data_targets(definitions: dict):
|
||||
|
||||
|
||||
def infer_data_sources(plot_id, config=None):
|
||||
y = config.get("y", None)
|
||||
y = config.get("y", None) if config else None
|
||||
|
||||
if isinstance(y, dict):
|
||||
sources = list(y.keys())
|
||||
else:
|
||||
sources = [plot_id]
|
||||
|
||||
x = config.get("x", None)
|
||||
x = config.get("x", None) if config else None
|
||||
if isinstance(x, dict):
|
||||
sources.append(first(x.keys()))
|
||||
|
||||
@@ -507,7 +507,7 @@ def _collect_pipeline_files(repo, targets: list[str], props, onerror=None):
|
||||
@error_handler
|
||||
def _collect_definitions(
|
||||
repo: "Repo",
|
||||
targets=None,
|
||||
targets: list[str],
|
||||
props: Optional[dict] = None,
|
||||
onerror: Optional[Callable] = None,
|
||||
**kwargs,
|
||||
|
||||
+7
-5
@@ -39,7 +39,7 @@ def pull( # noqa: PLR0913
|
||||
run_cache=run_cache,
|
||||
)
|
||||
try:
|
||||
stats = self.checkout(
|
||||
result = self.checkout(
|
||||
targets=expanded_targets,
|
||||
with_deps=with_deps,
|
||||
force=force,
|
||||
@@ -47,8 +47,10 @@ def pull( # noqa: PLR0913
|
||||
allow_missing=allow_missing,
|
||||
)
|
||||
except CheckoutError as exc:
|
||||
exc.stats["fetched"] = processed_files_count
|
||||
# put fetched counts first
|
||||
exc.result["stats"] = {"fetched": processed_files_count} | exc.result["stats"]
|
||||
raise
|
||||
|
||||
stats["fetched"] = processed_files_count
|
||||
return stats
|
||||
else:
|
||||
# put fetched counts first
|
||||
result["stats"] = {"fetched": processed_files_count} | result["stats"]
|
||||
return result
|
||||
|
||||
+4
-2
@@ -152,10 +152,12 @@ def push( # noqa: PLR0913
|
||||
finally:
|
||||
ws_idx = indexes.get("workspace")
|
||||
if ws_idx is not None:
|
||||
from dvc.repo.index import IndexView
|
||||
|
||||
_index = ws_idx.index if isinstance(ws_idx, IndexView) else ws_idx
|
||||
_update_meta(
|
||||
self.index,
|
||||
_index,
|
||||
targets=glob_targets(targets, glob=glob),
|
||||
remote=remote,
|
||||
with_deps=with_deps,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
@@ -41,8 +41,10 @@ class SCMContext:
|
||||
def add(self, paths: Union[str, Iterable[str]]) -> None:
|
||||
from scmrepo.exceptions import UnsupportedIndexFormat
|
||||
|
||||
from dvc.scm import add_no_submodules
|
||||
|
||||
try:
|
||||
return self.scm.add(paths)
|
||||
add_no_submodules(self.scm, paths)
|
||||
except UnsupportedIndexFormat:
|
||||
link = "https://github.com/iterative/dvc/issues/610"
|
||||
add_cmd = self._make_git_add_cmd([relpath(path) for path in paths])
|
||||
@@ -56,7 +58,7 @@ class SCMContext:
|
||||
def track_changed_files(self) -> None:
|
||||
"""Stage files that have changed."""
|
||||
if not self.files_to_track:
|
||||
return
|
||||
return None
|
||||
logger.debug("Staging files: %s", self.files_to_track)
|
||||
return self.add(self.files_to_track)
|
||||
|
||||
@@ -74,6 +76,7 @@ class SCMContext:
|
||||
logger.debug("Added '%s' to gitignore file.", path)
|
||||
self.track_file(relpath(gitignore_file))
|
||||
return self.ignored_paths.append(path)
|
||||
return None
|
||||
|
||||
def ignore_remove(self, path: str) -> None:
|
||||
from scmrepo.exceptions import FileNotInRepoError
|
||||
@@ -88,6 +91,7 @@ class SCMContext:
|
||||
|
||||
if gitignore_file:
|
||||
return self.track_file(relpath(gitignore_file))
|
||||
return None
|
||||
|
||||
@contextmanager
|
||||
def __call__(
|
||||
|
||||
+1
-1
@@ -130,7 +130,7 @@ def status( # noqa: PLR0913
|
||||
remote=remote,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
recursive=True,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
ignored = list(
|
||||
|
||||
@@ -152,7 +152,7 @@ def _merge_push_meta( # noqa: C901
|
||||
else:
|
||||
if entry.hash_info:
|
||||
out.hash_info = entry.hash_info
|
||||
if out.meta.version_id is None:
|
||||
if out.meta is None or out.meta.version_id is None:
|
||||
out.meta = entry.meta
|
||||
if out.meta:
|
||||
out.meta.remote = remote
|
||||
@@ -181,7 +181,7 @@ def _update_worktree_out(
|
||||
):
|
||||
from dvc_data.index import build
|
||||
|
||||
remote_name = out.remote or out.meta.remote
|
||||
remote_name = out.remote or (out.meta.remote if out.meta is not None else None)
|
||||
if not remote_name:
|
||||
logger.warning("Could not update '%s', it was never pushed to a remote", out)
|
||||
return
|
||||
@@ -212,11 +212,12 @@ def _update_worktree_out(
|
||||
logger.warning("Could not update '%s', directory is empty in the remote", out)
|
||||
return
|
||||
|
||||
_fetch_out_changes(out, local_index, remote_index, remote)
|
||||
_fetch_out_changes(repo, out, local_index, remote_index, remote)
|
||||
_update_out_meta(repo, out, local_index, remote_index, remote)
|
||||
|
||||
|
||||
def _fetch_out_changes(
|
||||
repo: "Repo",
|
||||
out: "Output",
|
||||
local_index: Union["DataIndex", "DataIndexView"],
|
||||
remote_index: Union["DataIndex", "DataIndexView"],
|
||||
@@ -242,7 +243,7 @@ def _fetch_out_changes(
|
||||
cb.set_size(total)
|
||||
apply(
|
||||
diff,
|
||||
out.repo.root_dir,
|
||||
repo.root_dir,
|
||||
out.fs,
|
||||
update_meta=False,
|
||||
storage="data",
|
||||
|
||||
+38
-3
@@ -1,17 +1,18 @@
|
||||
"""Manages source control systems (e.g. Git)."""
|
||||
|
||||
import os
|
||||
from collections.abc import Iterator, Mapping
|
||||
from collections.abc import Iterable, Iterator, Mapping
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Literal, Optional, Union, overload
|
||||
|
||||
from funcy import group_by
|
||||
from scmrepo.base import Base # noqa: F401
|
||||
from scmrepo.base import Base # noqa: TC002
|
||||
from scmrepo.git import Git
|
||||
from scmrepo.noscm import NoSCM
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.log import logger
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -19,6 +20,8 @@ if TYPE_CHECKING:
|
||||
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
class SCMError(DvcException):
|
||||
"""Base class for source control management errors."""
|
||||
@@ -42,7 +45,7 @@ class NoSCMError(SCMError):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class InvalidRemoteSCMRepo(SCMError):
|
||||
class InvalidRemoteSCMRepo(SCMError): # noqa: N818
|
||||
pass
|
||||
|
||||
|
||||
@@ -283,3 +286,35 @@ def lfs_prefetch(fs: "FileSystem", paths: list[str]):
|
||||
include=[(path if path.startswith("/") else f"/{path}") for path in paths],
|
||||
progress=pbar.update_git,
|
||||
)
|
||||
|
||||
|
||||
def add_no_submodules(
|
||||
scm: "Base",
|
||||
paths: Union[str, Iterable[str]],
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Stage paths to Git, excluding those inside submodules."""
|
||||
|
||||
if isinstance(paths, str):
|
||||
paths = [paths]
|
||||
|
||||
submodule_roots = {os.path.join(scm.root_dir, sub) for sub in scm.list_submodules()}
|
||||
|
||||
repo_paths: list[str] = []
|
||||
skipped_paths: list[str] = []
|
||||
|
||||
for p in paths:
|
||||
abs_path = os.path.abspath(p)
|
||||
if any(
|
||||
abs_path == root or abs_path.startswith(root + os.sep)
|
||||
for root in submodule_roots
|
||||
):
|
||||
skipped_paths.append(p)
|
||||
else:
|
||||
repo_paths.append(p)
|
||||
|
||||
if skipped_paths:
|
||||
msg = "Skipping staging for path(s) inside submodules: %s"
|
||||
logger.debug(msg, ", ".join(skipped_paths))
|
||||
|
||||
scm.add(repo_paths, **kwargs)
|
||||
|
||||
+9
-22
@@ -121,7 +121,7 @@ def restore_fields(stage: "Stage") -> None:
|
||||
|
||||
old_outs = {out.def_path: out for out in old.outs}
|
||||
for out in stage.outs:
|
||||
old_out = old_outs.get(out.def_path, None)
|
||||
old_out = old_outs.get(out.def_path)
|
||||
if old_out is not None:
|
||||
out.restore_fields(old_out)
|
||||
|
||||
@@ -509,34 +509,20 @@ class Stage(params.StageParams):
|
||||
if not allow_missing:
|
||||
raise
|
||||
|
||||
def get_versioned_outs(self) -> dict[str, "Output"]:
|
||||
from .exceptions import StageFileDoesNotExistError, StageNotFound
|
||||
|
||||
try:
|
||||
old = self.reload()
|
||||
except (StageFileDoesNotExistError, StageNotFound):
|
||||
return {}
|
||||
|
||||
return {
|
||||
out.def_path: out
|
||||
for out in old.outs
|
||||
if out.files is not None
|
||||
or (out.meta is not None and out.meta.version_id is not None)
|
||||
}
|
||||
|
||||
def save_outs(self, allow_missing: bool = False):
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
old_versioned_outs = self.get_versioned_outs()
|
||||
for out in self.outs:
|
||||
# old state just before saving so to merge them later
|
||||
old_state = out._get_versioned_meta()
|
||||
try:
|
||||
out.save()
|
||||
except OutputDoesNotExistError:
|
||||
if not allow_missing:
|
||||
raise
|
||||
|
||||
if old_out := old_versioned_outs.get(out.def_path):
|
||||
out.merge_version_meta(old_out)
|
||||
if old_state:
|
||||
out.merge_version_meta(*old_state)
|
||||
|
||||
def ignore_outs(self) -> None:
|
||||
for out in self.outs:
|
||||
@@ -579,8 +565,9 @@ class Stage(params.StageParams):
|
||||
from dvc.output import OutputDoesNotExistError
|
||||
|
||||
link_failures = []
|
||||
old_versioned_outs = self.get_versioned_outs()
|
||||
for out in self.filter_outs(filter_info):
|
||||
# old state just before saving so to merge them later
|
||||
old_state = out._get_versioned_meta()
|
||||
try:
|
||||
out.add(filter_info, **kwargs)
|
||||
except (FileNotFoundError, OutputDoesNotExistError):
|
||||
@@ -589,8 +576,8 @@ class Stage(params.StageParams):
|
||||
except CacheLinkError:
|
||||
link_failures.append(filter_info or out.fs_path)
|
||||
|
||||
if old_out := old_versioned_outs.get(out.def_path):
|
||||
out.merge_version_meta(old_out)
|
||||
if old_state:
|
||||
out.merge_version_meta(*old_state)
|
||||
|
||||
if link_failures:
|
||||
raise CacheLinkError(link_failures)
|
||||
|
||||
@@ -82,7 +82,7 @@ class DataSourceChanged(DvcException):
|
||||
super().__init__(f"data source changed: {path}")
|
||||
|
||||
|
||||
class StageNotFound(DvcException, KeyError):
|
||||
class StageNotFound(DvcException, KeyError): # noqa: N818
|
||||
def __init__(self, file, name):
|
||||
self.file = file.relpath
|
||||
self.name = name
|
||||
|
||||
@@ -168,6 +168,7 @@ def to_single_stage_lockfile(stage: "Stage", **kwargs) -> dict:
|
||||
for f in _serialize_tree_obj_to_files(obj)
|
||||
]
|
||||
else:
|
||||
assert item.meta is not None
|
||||
meta_d = item.meta.to_dict()
|
||||
meta_d.pop("isdir", None)
|
||||
ret.update(_serialize_hi_to_dict(item.hash_info))
|
||||
|
||||
@@ -158,7 +158,7 @@ def make_bench(request):
|
||||
# pytest >= 8.4.0
|
||||
wrapped_func = fixture_function._get_wrapped_function()
|
||||
except AttributeError:
|
||||
wrapped_func = fixture_function.__pytest_wrapped__.obj
|
||||
wrapped_func = fixture_function.__pytest_wrapped__.obj # ty: ignore[unresolved-attribute]
|
||||
assert inspect.isgeneratorfunction(wrapped_func)
|
||||
|
||||
generator = wrapped_func(request)
|
||||
|
||||
@@ -137,8 +137,12 @@ class TestRemote:
|
||||
remove(tmp_dir / "bar")
|
||||
|
||||
stats = dvc.pull()
|
||||
assert stats["fetched"] == 2
|
||||
assert set(stats["added"]) == {"foo", "bar"}
|
||||
assert stats == {
|
||||
"added": ["bar", "foo"],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
"stats": {"fetched": 2, "added": 2, "deleted": 0, "modified": 0},
|
||||
}
|
||||
|
||||
@pytest.mark.xfail(raises=NotImplementedError, strict=False)
|
||||
def test_pull_no_00_prefix(self, tmp_dir, dvc, remote, monkeypatch):
|
||||
@@ -163,8 +167,12 @@ class TestRemote:
|
||||
remove(tmp_dir / "bar")
|
||||
|
||||
stats = dvc.pull()
|
||||
assert stats["fetched"] == 2
|
||||
assert set(stats["added"]) == {"foo", "bar"}
|
||||
assert stats == {
|
||||
"added": ["bar", "foo"],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
"stats": {"fetched": 2, "added": 2, "deleted": 0, "modified": 0},
|
||||
}
|
||||
|
||||
|
||||
class TestRemoteVersionAware:
|
||||
|
||||
@@ -163,7 +163,12 @@ class TestImportURLVersionAware:
|
||||
|
||||
(remote_version_aware / "data_dir" / "foo").write_text("foo")
|
||||
dvc.update(no_download=True)
|
||||
assert dvc.pull()["fetched"] == 2
|
||||
assert dvc.pull() == {
|
||||
"modified": [],
|
||||
"added": ["data_dir" + os.sep],
|
||||
"deleted": [],
|
||||
"stats": {"fetched": 2, "modified": 0, "added": 2, "deleted": 0},
|
||||
}
|
||||
assert (tmp_dir / "data_dir").read_text() == {
|
||||
"foo": "foo",
|
||||
"subdir": {"file": "file"},
|
||||
@@ -174,7 +179,12 @@ class TestImportURLVersionAware:
|
||||
scm.checkout("v1")
|
||||
dvc.cache.local.clear()
|
||||
remove(tmp_dir / "data_dir")
|
||||
assert dvc.pull()["fetched"] == 1
|
||||
assert dvc.pull() == {
|
||||
"modified": [],
|
||||
"added": ["data_dir" + os.sep],
|
||||
"deleted": [],
|
||||
"stats": {"fetched": 1, "modified": 0, "added": 1, "deleted": 0},
|
||||
}
|
||||
assert (tmp_dir / "data_dir").read_text() == {"subdir": {"file": "file"}}
|
||||
|
||||
dvc.commit(force=True)
|
||||
|
||||
+3
-3
@@ -193,7 +193,7 @@ class Console:
|
||||
sep = " " if sep is None else sep
|
||||
end = "\n" if end is None else end
|
||||
if not self._enabled and not force:
|
||||
return
|
||||
return None
|
||||
|
||||
file = file or (sys.stderr if stderr else sys.stdout)
|
||||
with Tqdm.external_write_mode(file=file):
|
||||
@@ -305,7 +305,7 @@ class Console:
|
||||
from dvc.ui import table as t
|
||||
|
||||
if not data and not markdown:
|
||||
return
|
||||
return None
|
||||
|
||||
if not markdown and rich_table:
|
||||
if force or self._enabled:
|
||||
@@ -319,7 +319,7 @@ class Console:
|
||||
borders=borders,
|
||||
)
|
||||
|
||||
return
|
||||
return None
|
||||
|
||||
return t.plain_table(
|
||||
self,
|
||||
|
||||
@@ -44,6 +44,7 @@ def prepare_default_pager(
|
||||
|
||||
def make_pager(cmd=None):
|
||||
def _pager(text):
|
||||
assert cmd
|
||||
return pydoc.tempfilepager(pydoc.plain(text), cmd)
|
||||
|
||||
return _pager if cmd else pydoc.plainpager
|
||||
|
||||
+3
-2
@@ -99,13 +99,14 @@ class Updater:
|
||||
import json
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
url = os.environ.get(DVC_UPDATER_ENDPOINT, self.URL)
|
||||
logger.debug("Checking updates in %s", url)
|
||||
try:
|
||||
resp = requests.get(url, timeout=self.TIMEOUT_GET)
|
||||
info = resp.json()
|
||||
except requests.exceptions.RequestException as exc:
|
||||
except RequestException as exc:
|
||||
logger.trace("", exc_info=True)
|
||||
logger.debug("Failed to retrieve latest version: %s", exc)
|
||||
return
|
||||
@@ -119,7 +120,7 @@ class Updater:
|
||||
from dvc.ui import ui
|
||||
|
||||
if not ui.isatty():
|
||||
return
|
||||
return None
|
||||
|
||||
message = self._get_message(latest, pkg=pkg)
|
||||
return ui.error_write(message, styled=True)
|
||||
|
||||
@@ -8,6 +8,7 @@ import sys
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import colorama
|
||||
from colorama import AnsiToWin32
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TextIO
|
||||
@@ -18,7 +19,7 @@ TARGET_REGEX = re.compile(r"(?P<path>.*?)(:(?P<name>[^\\/:]*))??$")
|
||||
|
||||
|
||||
def bytes_hash(byts, typ):
|
||||
hasher = getattr(hashlib, typ)()
|
||||
hasher = getattr(hashlib, typ)(usedforsecurity=False)
|
||||
hasher.update(byts)
|
||||
return hasher.hexdigest()
|
||||
|
||||
@@ -205,7 +206,7 @@ def boxify(message, border_color=None):
|
||||
def _visual_width(line):
|
||||
"""Get the number of columns required to display a string"""
|
||||
|
||||
return len(re.sub(colorama.ansitowin32.AnsiToWin32.ANSI_CSI_RE, "", line))
|
||||
return len(re.sub(AnsiToWin32.ANSI_CSI_RE, "", line))
|
||||
|
||||
|
||||
def _visual_center(line, width):
|
||||
|
||||
+24
-14
@@ -20,6 +20,7 @@ from dvc.utils.serialize import (
|
||||
parse_yaml,
|
||||
parse_yaml_for_update,
|
||||
)
|
||||
from dvc_objects.fs.local import LocalFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.syntax import Syntax
|
||||
@@ -34,15 +35,24 @@ _T = TypeVar("_T")
|
||||
merge_conflict_marker = re.compile("^([<=>]{7}) .*$", re.MULTILINE)
|
||||
|
||||
|
||||
def make_relpath(path: str) -> str:
|
||||
import os
|
||||
def make_relpath(fs_path: str, fs: "Optional[FileSystem]" = None) -> str:
|
||||
from os import curdir, pardir, sep
|
||||
|
||||
from dvc.utils import relpath
|
||||
|
||||
rel = relpath(path)
|
||||
prefix = ""
|
||||
if not rel.startswith(".."):
|
||||
prefix = "./" if os.name == "posix" else ".\\"
|
||||
if not fs_path:
|
||||
return fs_path
|
||||
|
||||
if fs and not isinstance(fs, LocalFileSystem):
|
||||
rel = fs.relpath(fs_path).replace(fs.sep, sep)
|
||||
else:
|
||||
rel = relpath(fs_path)
|
||||
|
||||
if rel.startswith(sep):
|
||||
# if it is an absolute path, return as it is.
|
||||
# This can happen on Windows when we are on a different drive.
|
||||
return rel
|
||||
prefix = curdir + sep if not rel.startswith(pardir) else ""
|
||||
return prefix + rel
|
||||
|
||||
|
||||
@@ -77,14 +87,14 @@ class YAMLSyntaxError(PrettyDvcException, YAMLFileCorruptedError):
|
||||
exc: Exception,
|
||||
rev: Optional[str] = None,
|
||||
) -> None:
|
||||
self.path: str = path
|
||||
self.yaml_text: str = yaml_text
|
||||
self.exc: Exception = exc
|
||||
|
||||
merge_conflicts = merge_conflict_marker.search(self.yaml_text)
|
||||
self.hint = " (possible merge conflicts)" if merge_conflicts else ""
|
||||
self.rev: Optional[str] = rev
|
||||
super().__init__(self.path)
|
||||
super().__init__(path)
|
||||
self.relpath: str = path
|
||||
|
||||
def __pretty_exc__(self, **kwargs: Any) -> None: # noqa: C901
|
||||
from ruamel.yaml.error import MarkedYAMLError
|
||||
@@ -134,9 +144,8 @@ class YAMLSyntaxError(PrettyDvcException, YAMLFileCorruptedError):
|
||||
# if there are no other outputs
|
||||
lines.insert(0, "")
|
||||
|
||||
rel = make_relpath(self.path)
|
||||
rev_msg = f" in revision '{self.rev[:7]}'" if self.rev else ""
|
||||
msg_fmt = f"'{rel}' is invalid{self.hint}{rev_msg}."
|
||||
msg_fmt = f"'{self.relpath}' is invalid{self.hint}{rev_msg}."
|
||||
lines.insert(0, _prepare_message(msg_fmt))
|
||||
for line in lines:
|
||||
ui.error_write(line, styled=True)
|
||||
@@ -198,10 +207,9 @@ class YAMLValidationError(PrettyDvcException):
|
||||
self.text = text or ""
|
||||
self.exc = exc
|
||||
|
||||
rel = make_relpath(path) if path else ""
|
||||
self.path = path or ""
|
||||
|
||||
message = f"'{rel}' validation failed"
|
||||
message = f"'{self.path}' validation failed"
|
||||
message += f" in revision '{rev[:7]}'" if rev else ""
|
||||
if len(self.exc.errors) > 1:
|
||||
message += f": {len(self.exc.errors)} errors"
|
||||
@@ -288,10 +296,12 @@ def load(
|
||||
raise EncodingError(path, encoding) from exc
|
||||
except YAMLFileCorruptedError as exc:
|
||||
cause = exc.__cause__
|
||||
raise YAMLSyntaxError(path, text, exc, rev=rev) from cause
|
||||
relpath = make_relpath(path, fs)
|
||||
raise YAMLSyntaxError(relpath, text, exc, rev=rev) from cause
|
||||
|
||||
if schema:
|
||||
relpath = make_relpath(path, fs)
|
||||
# not returning validated data, as it may remove
|
||||
# details from CommentedMap that we get from roundtrip parser
|
||||
validate(data, schema, text=text, path=path, rev=rev)
|
||||
validate(data, schema, text=text, path=relpath, rev=rev)
|
||||
return data, text
|
||||
|
||||
+16
-8
@@ -67,9 +67,10 @@ dependencies = [
|
||||
"requests>=2.22",
|
||||
"rich>=12",
|
||||
"ruamel.yaml>=0.17.11",
|
||||
"scmrepo>=3.3.8,<4",
|
||||
"scmrepo>=3.5.2,<4",
|
||||
"shortuuid>=0.5",
|
||||
"shtab<2,>=1.3.4",
|
||||
# "shtab<2,>=1.3.4",
|
||||
"shtab @ git+https://github.com/skshetry/shtab.git@fish-shell-v2",
|
||||
"tabulate>=0.8.7",
|
||||
"tomlkit>=0.11.1",
|
||||
"tqdm<5,>=4.63.1",
|
||||
@@ -85,7 +86,7 @@ gdrive = ["dvc-gdrive>=3,<4"]
|
||||
gs = ["dvc-gs>=3.0.2,<4"]
|
||||
hdfs = ["dvc-hdfs>=3,<4"]
|
||||
lint = [
|
||||
"mypy==1.16.1",
|
||||
"mypy==1.18.2",
|
||||
"pandas-stubs",
|
||||
"types-colorama",
|
||||
"types-psutil",
|
||||
@@ -111,7 +112,7 @@ tests = [
|
||||
"dvc-ssh",
|
||||
"filelock",
|
||||
"pytest>=7,<9",
|
||||
"pytest-rerunfailures",
|
||||
"pytest-rerunfailures<17.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
"pytest-docker>=1,<4",
|
||||
"pytest-mock",
|
||||
@@ -175,7 +176,6 @@ filterwarnings = [
|
||||
"ignore:unclosed database.*sqlite3.Connection:ResourceWarning",
|
||||
"ignore:unclosed.*<socket.socket:ResourceWarning",
|
||||
]
|
||||
log_level = "debug"
|
||||
markers = [
|
||||
"needs_internet: Might need network access for the tests",
|
||||
"studio: Tests verifying contract between DVC and Studio",
|
||||
@@ -266,8 +266,16 @@ show-fixes = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = [
|
||||
"N818", "S101", "A005", "PT007", "RET502", "RET503", "SIM105", "SIM108", "SIM117",
|
||||
"TRY003", "TRY300", "PERF203", "PLC0415", "PLR2004", "PLW2901", "LOG007",
|
||||
"PERF203", # try-except-in-loop
|
||||
"PLC0415", # import-outside-top-level
|
||||
"PLR2004", # magic-value-comparison
|
||||
"PLW2901", # redefined-loop-name
|
||||
"S101", # assert
|
||||
"SIM105", # suppressible-exception
|
||||
"SIM108", # if-else-block-instead-of-if-exp
|
||||
"SIM117", # multiple-with-statements
|
||||
"TRY003", # raise-vanilla-args
|
||||
"TRY300", # try-consider-else
|
||||
]
|
||||
select = [
|
||||
"F", "E", "W", "C90", "I", "N", "UP", "YTT", "ASYNC", "S", "BLE", "B", "A", "C4", "DTZ", "T10",
|
||||
@@ -305,4 +313,4 @@ max-args = 10
|
||||
"dvc/commands/**" = ["N806"]
|
||||
"dvc/testing/**" = ["ARG002"]
|
||||
"dvc/testing/benchmarks/**" = ["ARG001"]
|
||||
"tests/**" = ["S", "ARG001", "ARG002", "TRY002", "TRY301", "PERF"]
|
||||
"tests/**" = ["S", "ARG001", "ARG002", "TRY002", "TRY301", "PERF", "PLR2004"]
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from contextlib import suppress
|
||||
from contextlib import ExitStack, suppress
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -46,10 +46,10 @@ def reset_loglevel(request, caplog):
|
||||
ini_opt = request.config.getini("log_level")
|
||||
|
||||
level = request.config.getoption("--log-level") or ini_opt
|
||||
if level:
|
||||
with caplog.at_level(level.upper(), logger="dvc"):
|
||||
yield
|
||||
else:
|
||||
with ExitStack() as stack:
|
||||
if level:
|
||||
for name in ["dvc", "dvc_data", "dvc_objects"]:
|
||||
stack.enter_context(caplog.at_level(level.upper(), logger=name))
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ from global repo template to creating everything inplace, which:
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from dulwich.porcelain import remote_add as git_remote_add
|
||||
|
||||
__all__ = [
|
||||
"erepo_dir",
|
||||
@@ -96,7 +97,7 @@ class GitRemote:
|
||||
def git_upstream(tmp_dir, erepo_dir, git_dir, request):
|
||||
remote = erepo_dir if "dvc" in request.fixturenames else git_dir
|
||||
url = f"file://{remote.resolve().as_posix()}"
|
||||
tmp_dir.scm.gitpython.repo.create_remote("upstream", url)
|
||||
git_remote_add(tmp_dir, "upstream", url)
|
||||
return GitRemote(remote, "upstream", url)
|
||||
|
||||
|
||||
@@ -104,5 +105,5 @@ def git_upstream(tmp_dir, erepo_dir, git_dir, request):
|
||||
def git_downstream(tmp_dir, erepo_dir, git_dir, request):
|
||||
remote = erepo_dir if "dvc" in request.fixturenames else git_dir
|
||||
url = f"file://{tmp_dir.resolve().as_posix()}"
|
||||
remote.scm.gitpython.repo.create_remote("upstream", url)
|
||||
git_remote_add(remote, "upstream", url)
|
||||
return GitRemote(remote, "upstream", url)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc import api
|
||||
@@ -13,7 +15,7 @@ def test_exp_save(tmp_dir, dvc, scm):
|
||||
api.exp_save("foo")
|
||||
with pytest.raises(
|
||||
ExperimentExistsError,
|
||||
match="Experiment conflicts with existing experiment 'foo'.",
|
||||
match=re.escape("Experiment conflicts with existing experiment 'foo'."),
|
||||
):
|
||||
api.exp_save("foo")
|
||||
api.exp_save("foo", force=True)
|
||||
|
||||
@@ -4,6 +4,7 @@ from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
|
||||
from dvc import env
|
||||
from dvc.annotations import Artifact
|
||||
from dvc.exceptions import ArtifactNotFoundError, InvalidArgumentError
|
||||
from dvc.repo.artifacts import Artifacts, check_name_format
|
||||
@@ -283,7 +284,13 @@ def test_artifacts_download_subrepo(tmp_dir, scm, sub):
|
||||
assert Artifacts.get(".", name, force=True) == result
|
||||
|
||||
|
||||
def test_artifacts_download_studio(tmp_dir, dvc, mocker):
|
||||
def test_artifacts_download_studio(monkeypatch, tmp_dir, dvc, mocker, tmp_path_factory):
|
||||
global_config_dir = tmp_path_factory.mktemp("global_config")
|
||||
monkeypatch.setenv(env.DVC_GLOBAL_CONFIG_DIR, str(global_config_dir))
|
||||
# reset config to ensure it picks up the new global config
|
||||
dvc.__dict__.pop("config")
|
||||
assert dvc.config.files["global"] == os.fspath(global_config_dir / "config")
|
||||
|
||||
with dvc.config.edit("global") as conf:
|
||||
conf["studio"]["token"] = "mytoken"
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import stat
|
||||
from textwrap import dedent
|
||||
|
||||
import dulwich
|
||||
import pytest
|
||||
from configobj import ConfigObj
|
||||
from funcy import first
|
||||
@@ -19,6 +20,7 @@ from dvc.env import (
|
||||
DVC_STUDIO_URL,
|
||||
)
|
||||
from dvc.exceptions import DvcException, ReproductionError
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.exceptions import ExperimentExistsError
|
||||
from dvc.repo.experiments.queue.base import BaseStashQueue
|
||||
from dvc.repo.experiments.refs import CELERY_STASH
|
||||
@@ -227,7 +229,7 @@ def test_detached_parent(tmp_dir, scm, dvc, exp_stage, mocker):
|
||||
scm.commit("v2")
|
||||
|
||||
scm.checkout(detached_rev)
|
||||
assert scm.gitpython.repo.head.is_detached
|
||||
assert scm.pygit2.repo.head_is_detached
|
||||
results = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
|
||||
|
||||
exp_rev = first(results)
|
||||
@@ -792,4 +794,42 @@ def test_custom_commit_message(tmp_dir, scm, dvc, tmp):
|
||||
stage.addressing, tmp_dir=tmp, message="custom commit message"
|
||||
)
|
||||
)
|
||||
assert scm.gitpython.repo.commit(exp).message == "custom commit message"
|
||||
assert scm.resolve_commit(exp).message == "custom commit message"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dep", ["submodule", "submodule/file"])
|
||||
def test_experiments_run_with_submodule_dependencies(dvc, scm, make_tmp_dir, dep):
|
||||
external_repo = make_tmp_dir("external_repo", scm=True)
|
||||
external_repo.scm_gen("file", "content", commit="add file")
|
||||
|
||||
submodules = scm.pygit2.repo.submodules
|
||||
submodules.add(os.fspath(external_repo), "submodule")
|
||||
submodules.update(init=True)
|
||||
scm.add_commit([".gitmodules"], message="add submodule")
|
||||
|
||||
dvc.stage.add(cmd="echo foo", deps=[dep], name="foo")
|
||||
|
||||
assert dvc.experiments.run()
|
||||
|
||||
|
||||
@pytest.mark.skipif(dulwich.__version__ < (0, 24, 2), reason="requires dulwich>=0.24.2")
|
||||
def test_experiments_run_in_linked_git_worktree(
|
||||
dvc, scm, tmp_path_factory: pytest.TempPathFactory, monkeypatch
|
||||
):
|
||||
from dulwich.worktree import add_worktree
|
||||
|
||||
wt = tmp_path_factory.mktemp("worktrees") / "worktree"
|
||||
add_worktree(scm.dulwich.repo, wt, branch="wt-main")
|
||||
|
||||
monkeypatch.chdir(wt)
|
||||
|
||||
wt_dvc = Repo(os.fspath(wt))
|
||||
(wt / "foo").write_bytes(b"foo")
|
||||
wt_dvc.stage.add(cmd="cp foo bar", deps=["foo"], outs=["bar"], name="cp")
|
||||
|
||||
results = wt_dvc.experiments.run(name="my-exp")
|
||||
assert results
|
||||
rev = first(results)
|
||||
assert rev
|
||||
# If `bar` exists, we know that the stage was run.
|
||||
assert (wt / "bar").read_bytes() == b"foo"
|
||||
|
||||
@@ -61,4 +61,4 @@ def test_custom_commit_message_queue(tmp_dir, scm, dvc):
|
||||
dvc.experiments.run(stage.addressing, queue=True, message="custom commit message")
|
||||
|
||||
exp = first(dvc.experiments.run(run_all=True))
|
||||
assert scm.gitpython.repo.commit(exp).message == "custom commit message"
|
||||
assert scm.resolve_commit(exp).message == "custom commit message"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from dulwich.porcelain import pull as git_pull
|
||||
from funcy import first
|
||||
|
||||
from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
@@ -180,10 +181,8 @@ def test_list_remote(tmp_dir, scm, dvc, git_downstream, exp_stage, use_url):
|
||||
def test_pull(tmp_dir, scm, dvc, git_downstream, exp_stage, use_url):
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
|
||||
# fetch and checkout to downstream so both repos start from same commit
|
||||
downstream_repo = git_downstream.tmp_dir.scm.gitpython.repo
|
||||
fetched = downstream_repo.remote(git_downstream.remote).fetch()
|
||||
downstream_repo.git.checkout(fetched)
|
||||
# pull to downstream so both repos start from same commit
|
||||
git_pull(git_downstream.tmp_dir, "upstream")
|
||||
|
||||
remote = git_downstream.url if use_url else git_downstream.remote
|
||||
downstream_exp = git_downstream.tmp_dir.dvc.experiments
|
||||
|
||||
@@ -204,10 +204,10 @@ def test_remove_multi_rev(tmp_dir, scm, dvc, exp_stage):
|
||||
@pytest.mark.parametrize(
|
||||
"keep, expected_removed",
|
||||
[
|
||||
[["exp1"], ["exp2", "exp3"]],
|
||||
[["exp1", "exp2"], ["exp3"]],
|
||||
[["exp1", "exp2", "exp3"], []],
|
||||
[[], []], # remove does nothing if no experiments are specified
|
||||
(["exp1"], ["exp2", "exp3"]),
|
||||
(["exp1", "exp2"], ["exp3"]),
|
||||
(["exp1", "exp2", "exp3"], []),
|
||||
([], []), # remove does nothing if no experiments are specified
|
||||
],
|
||||
)
|
||||
def test_keep_selected_by_name(tmp_dir, scm, dvc, exp_stage, keep, expected_removed):
|
||||
@@ -239,14 +239,14 @@ def test_keep_selected_by_nonexistent_name(tmp_dir, scm, dvc, exp_stage):
|
||||
@pytest.mark.parametrize(
|
||||
"num_exps, rev, num, expected_removed",
|
||||
[
|
||||
[2, "exp1", 1, ["exp2"]],
|
||||
[3, "exp3", 1, ["exp1", "exp2"]],
|
||||
[3, "exp3", 2, ["exp1"]],
|
||||
[3, "exp3", 3, []],
|
||||
[3, "exp2", 2, ["exp3"]],
|
||||
[4, "exp2", 2, ["exp3", "exp4"]],
|
||||
[4, "exp4", 2, ["exp1", "exp2"]],
|
||||
[1, None, 1, []], # remove does nothing if no experiments are specified
|
||||
(2, "exp1", 1, ["exp2"]),
|
||||
(3, "exp3", 1, ["exp1", "exp2"]),
|
||||
(3, "exp3", 2, ["exp1"]),
|
||||
(3, "exp3", 3, []),
|
||||
(3, "exp2", 2, ["exp3"]),
|
||||
(4, "exp2", 2, ["exp3", "exp4"]),
|
||||
(4, "exp4", 2, ["exp1", "exp2"]),
|
||||
(1, None, 1, []), # remove does nothing if no experiments are specified
|
||||
],
|
||||
)
|
||||
def test_keep_selected_by_rev(
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_exp_save_unchanged(tmp_dir, dvc, scm):
|
||||
dvc.experiments.save()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", (None, "test"))
|
||||
@pytest.mark.parametrize("name", [None, "test"])
|
||||
def test_exp_save(tmp_dir, dvc, scm, name):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
baseline = scm.get_rev()
|
||||
@@ -47,13 +47,13 @@ def test_exp_save_overwrite_experiment(tmp_dir, dvc, scm):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
(
|
||||
[
|
||||
"invalid/name",
|
||||
"invalid..name",
|
||||
"invalid~name",
|
||||
"invalid?name",
|
||||
"invalidname.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_exp_save_invalid_name(tmp_dir, dvc, scm, name):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
@@ -165,7 +165,7 @@ def test_exp_save_custom_message(tmp_dir, dvc, scm):
|
||||
setup_stage(tmp_dir, dvc, scm)
|
||||
|
||||
exp = dvc.experiments.save(message="custom commit message")
|
||||
assert scm.gitpython.repo.commit(exp).message == "custom commit message"
|
||||
assert scm.resolve_commit(exp).message == "custom commit message"
|
||||
|
||||
|
||||
def test_exp_save_target(tmp_dir, dvc, scm):
|
||||
|
||||
@@ -7,8 +7,8 @@ from tests.func.utils.test_hydra import hydra_setup
|
||||
@pytest.mark.parametrize(
|
||||
"changes, expected",
|
||||
[
|
||||
[["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
|
||||
[["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"],
|
||||
(["foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"),
|
||||
(["params.yaml:foo=baz"], "foo: baz\ngoo:\n bag: 3.0\nlorem: false"),
|
||||
],
|
||||
)
|
||||
def test_modify_params(params_repo, dvc, changes, expected):
|
||||
|
||||
@@ -87,14 +87,27 @@ def test_show_branch_and_tag_name(tmp_dir, scm, dvc, exp_stage):
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
def test_show_simple(tmp_dir, scm, dvc, exp_stage):
|
||||
assert dvc.experiments.show()[0].dumpd() == {
|
||||
def test_show(tmp_dir, scm, dvc, exp_stage):
|
||||
workspace_result = {
|
||||
"rev": "workspace",
|
||||
"name": None,
|
||||
"data": make_data(rev="workspace"),
|
||||
"error": None,
|
||||
"experiments": None,
|
||||
}
|
||||
branch_result = {
|
||||
"rev": scm.get_rev(),
|
||||
"name": "master",
|
||||
"data": make_data(rev=scm.get_rev()),
|
||||
"error": None,
|
||||
"experiments": None,
|
||||
}
|
||||
|
||||
ret = [d.dumpd() for d in dvc.experiments.show()]
|
||||
assert ret == [workspace_result, branch_result]
|
||||
|
||||
ret = [d.dumpd() for d in dvc.experiments.show(hide_workspace=True)]
|
||||
assert ret == [branch_result]
|
||||
|
||||
|
||||
@pytest.mark.vscode
|
||||
@@ -102,7 +115,7 @@ def test_show_simple(tmp_dir, scm, dvc, exp_stage):
|
||||
def test_show_experiment(tmp_dir, scm, dvc, exp_stage, workspace):
|
||||
baseline_rev = scm.get_rev()
|
||||
timestamp = datetime.fromtimestamp( # noqa: DTZ006
|
||||
scm.gitpython.repo.rev_parse(baseline_rev).committed_date
|
||||
scm.resolve_commit(baseline_rev).commit_time
|
||||
)
|
||||
|
||||
exp_rev = first(
|
||||
@@ -424,7 +437,7 @@ def test_show_csv(tmp_dir, scm, dvc, exp_stage, capsys):
|
||||
|
||||
def _get_rev_isotimestamp(rev):
|
||||
return datetime.fromtimestamp( # noqa: DTZ006
|
||||
scm.gitpython.repo.rev_parse(rev).committed_date
|
||||
scm.resolve_commit(rev).commit_time
|
||||
).isoformat()
|
||||
|
||||
result1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
@@ -653,7 +666,7 @@ def test_metrics_renaming(tmp_dir, dvc, scm, capsys, copy_script):
|
||||
|
||||
def _get_rev_isotimestamp(rev):
|
||||
return datetime.fromtimestamp( # noqa: DTZ006
|
||||
scm.gitpython.repo.rev_parse(rev).committed_date
|
||||
scm.resolve_commit(rev).commit_time
|
||||
).isoformat()
|
||||
|
||||
assert f",master,baseline,{_get_rev_isotimestamp(scores_rev)},,1,,1" in cap.out
|
||||
|
||||
@@ -289,14 +289,14 @@ def test_metrics_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file,error_path,err_type",
|
||||
(
|
||||
[
|
||||
(PROJECT_FILE, ["workspace", "error", "type"], "YAMLSyntaxError"),
|
||||
(
|
||||
"metrics.yaml",
|
||||
["workspace", "data", "metrics.yaml", "error", "type"],
|
||||
"YAMLFileCorruptedError",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_log_errors(
|
||||
tmp_dir, scm, dvc, capsys, run_copy_metrics, file, error_path, err_type
|
||||
|
||||
@@ -300,13 +300,13 @@ def test_ignore_parsing_error(tmp_dir, dvc, run_copy_metrics):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"file,path_kwargs",
|
||||
(
|
||||
[
|
||||
(PROJECT_FILE, {"revision": "workspace", "endkey": "error"}),
|
||||
(
|
||||
"plot.yaml",
|
||||
{"revision": "workspace", "file": "plot.yaml", "endkey": "error"},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_log_errors(tmp_dir, scm, dvc, run_copy_metrics, file, path_kwargs, capsys):
|
||||
metric = [{"val": 2}, {"val": 3}]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user