mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 07:18:25 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6e3912da0 | ||
|
|
7f6890ff25 | ||
|
|
99daf5d113 | ||
|
|
83ffb8803e | ||
|
|
ace8464acb | ||
|
|
0dbdfd2b9b | ||
|
|
6d4251798a | ||
|
|
11d031935d | ||
|
|
64beb1578a | ||
|
|
f888544554 | ||
|
|
1e538ed87b | ||
|
|
e69deaa132 | ||
|
|
be0a1a4a12 | ||
|
|
004f49744e | ||
|
|
45181504ea | ||
|
|
915dd5062e | ||
|
|
d08c85f7d1 | ||
|
|
2342099bd8 | ||
|
|
06d8e3c639 | ||
|
|
195e67c0c1 | ||
|
|
c75a5583b3 | ||
|
|
981baa8c30 | ||
|
|
aa517bc90e | ||
|
|
c74dc20b97 | ||
|
|
1ba3944944 | ||
|
|
8620b6e5af | ||
|
|
6b6084a848 | ||
|
|
967dd5ea2b | ||
|
|
ad39bf14ed | ||
|
|
01416df5a4 | ||
|
|
eb592e7e33 | ||
|
|
2db07b047e | ||
|
|
8ecc66ae9a | ||
|
|
dfa5f4d9a0 | ||
|
|
75952c4e36 | ||
|
|
31b0b8de73 | ||
|
|
fae47e6122 | ||
|
|
ac3eef439f | ||
|
|
b044bcce5e | ||
|
|
a251cffdb5 | ||
|
|
37858be02f | ||
|
|
9b7240e9e0 | ||
|
|
e362438f03 | ||
|
|
6c79f6bbfc | ||
|
|
58cd1cbc09 | ||
|
|
96e3b48afd | ||
|
|
1f03c40dbe | ||
|
|
e93e8f05a3 | ||
|
|
0f1326e547 | ||
|
|
2a04263f02 | ||
|
|
348446050e | ||
|
|
7e1036b30d | ||
|
|
a15dbc8e03 | ||
|
|
4e02197245 | ||
|
|
4ab7ef06e5 | ||
|
|
dae2faf36d | ||
|
|
58f10a4661 | ||
|
|
64e787fa02 | ||
|
|
c7e2ec9707 | ||
|
|
a4cc198ac7 | ||
|
|
d203bf9024 | ||
|
|
baeec8ec2d | ||
|
|
9c689a3b51 | ||
|
|
e1bf418e72 | ||
|
|
b6d8c07d4d | ||
|
|
b814531c84 | ||
|
|
3b3cd0a0b2 | ||
|
|
b2101a85ad | ||
|
|
ba760c56e5 | ||
|
|
787c72ea18 | ||
|
|
a2b7a59322 | ||
|
|
ec090c5680 | ||
|
|
58f74ae543 | ||
|
|
2e8bcbf719 | ||
|
|
10f523430b | ||
|
|
5563a595d5 | ||
|
|
dd3deb6cbd | ||
|
|
7841020b2b | ||
|
|
1c394986db | ||
|
|
870935ebdc | ||
|
|
1be82fa56d | ||
|
|
ce0f6c187e | ||
|
|
559ec392f6 | ||
|
|
c71a4082e6 | ||
|
|
ce269ffa58 | ||
|
|
61d0893d7c | ||
|
|
eef5fb9b37 | ||
|
|
f03d555747 | ||
|
|
a91993616f | ||
|
|
3c570a2177 | ||
|
|
3312a90949 | ||
|
|
32bebc0246 | ||
|
|
35648134f7 | ||
|
|
df09e522ec | ||
|
|
40f7f34889 | ||
|
|
cf52efb8c3 | ||
|
|
8be71be540 | ||
|
|
071a5b7ba4 | ||
|
|
ed6cf093a8 | ||
|
|
7c3c47b4a2 | ||
|
|
3a2259821b | ||
|
|
649ee67e4b | ||
|
|
7cdb3b0ae2 | ||
|
|
ecdc44e010 | ||
|
|
24e10e0278 | ||
|
|
7c8e6e3156 | ||
|
|
d2812fa48f | ||
|
|
086d51d9de | ||
|
|
1d8b2e5fa6 | ||
|
|
894f530d22 | ||
|
|
1b9d40960f | ||
|
|
696fabc745 | ||
|
|
780dc72301 | ||
|
|
1ca3ef14a3 | ||
|
|
a6c89cac8d | ||
|
|
593f1064cf | ||
|
|
35bb261b89 | ||
|
|
fe6d543c1f | ||
|
|
4351d32de4 | ||
|
|
efc2d7ad3d | ||
|
|
7c65739e7e | ||
|
|
cc4816f652 | ||
|
|
faa1bdfbce | ||
|
|
006b6f3513 | ||
|
|
1bc4c1dceb | ||
|
|
531b2110cf | ||
|
|
c1917ed751 | ||
|
|
43195a806d | ||
|
|
d41512c85c | ||
|
|
e9fb0b92aa | ||
|
|
5485275b92 | ||
|
|
1f80f2f2ed | ||
|
|
6b6dc01ac4 | ||
|
|
91d3ff0f69 | ||
|
|
701f48197e | ||
|
|
da3bbccf66 | ||
|
|
40484e8d57 | ||
|
|
48b56c83dc | ||
|
|
f8bbf20b77 | ||
|
|
cd2c4d0caf | ||
|
|
9b329d01bf | ||
|
|
5c442cb867 | ||
|
|
2d5c15bee1 | ||
|
|
7868c1301c | ||
|
|
dfe76c54d7 | ||
|
|
392c15e1d4 | ||
|
|
121d56b739 | ||
|
|
66406e6c99 | ||
|
|
8399bf612a | ||
|
|
6c1b786564 | ||
|
|
d1c5fcc1b8 | ||
|
|
4ee3271b0c | ||
|
|
dae8515933 | ||
|
|
f02dd83e53 | ||
|
|
a2066d75f9 | ||
|
|
283d8245e1 | ||
|
|
c52b12ac2a | ||
|
|
440cb9d2bc | ||
|
|
2d820cccad | ||
|
|
30c3e3e4d4 | ||
|
|
58029e9719 | ||
|
|
990e834f1d | ||
|
|
33a2449c91 | ||
|
|
e2ce95e17e | ||
|
|
53f9953cd1 | ||
|
|
e6419b939d | ||
|
|
37ccc5e8d8 | ||
|
|
8312eb9b42 | ||
|
|
c8745dd580 | ||
|
|
1cf8b5e582 | ||
|
|
12955da3c3 | ||
|
|
e73cc9b40a | ||
|
|
b56f6b69dd | ||
|
|
54a6709d99 | ||
|
|
e4a5a2aec5 | ||
|
|
16ecd53b71 | ||
|
|
612f3039e0 | ||
|
|
17f523bf86 | ||
|
|
a0bf2d997a | ||
|
|
8ed9b911db | ||
|
|
ef22b1d77e | ||
|
|
aaa825a9a8 | ||
|
|
2a40a55bb4 | ||
|
|
3aa9f51e36 | ||
|
|
1cfdc22549 | ||
|
|
2b889baa5e | ||
|
|
8589aa4001 | ||
|
|
8cbc4c43f1 | ||
|
|
417c327143 | ||
|
|
204eebaf15 | ||
|
|
3a6fd75d98 | ||
|
|
351b952d5f | ||
|
|
fe7db32311 | ||
|
|
0bd34c8977 | ||
|
|
48a63ca588 | ||
|
|
4459d6ac57 | ||
|
|
08a50ee36e | ||
|
|
f1ea64c4ef | ||
|
|
7ede45af2d | ||
|
|
51db1765ee | ||
|
|
9101fce761 | ||
|
|
3f842a205f | ||
|
|
77e900fed2 | ||
|
|
dde456c923 | ||
|
|
cb9d3529af | ||
|
|
876c10fcf5 | ||
|
|
93eea0cd42 | ||
|
|
25fd274090 | ||
|
|
ddfa607437 | ||
|
|
c5a3566e49 | ||
|
|
e7d4f5b24e | ||
|
|
1d2892efac | ||
|
|
83835afa69 | ||
|
|
4c5d052d8c | ||
|
|
e4d514c990 | ||
|
|
baeecd1972 | ||
|
|
dbd098506c | ||
|
|
4779c248f8 | ||
|
|
2cf3a12e46 | ||
|
|
78fb5275e5 | ||
|
|
ccbe2de89e | ||
|
|
54e39e4abd | ||
|
|
59a4c4f67c | ||
|
|
1a7ae16f69 | ||
|
|
286f4fda3e | ||
|
|
d2c63bffcb | ||
|
|
2b41305c51 | ||
|
|
546becddaf | ||
|
|
c6015a05a3 | ||
|
|
c0754ee5b3 | ||
|
|
4db2e89938 | ||
|
|
4a6f3ed64a | ||
|
|
9f0c4350bd | ||
|
|
a91528c3c9 | ||
|
|
b80d84e689 | ||
|
|
2a2c02c184 | ||
|
|
35b648bbcf | ||
|
|
96ac321dd5 | ||
|
|
50fd8ca15d | ||
|
|
184cc35cf0 | ||
|
|
81da91a973 | ||
|
|
ad5b91e51a | ||
|
|
391cf80e7b | ||
|
|
8d13486048 | ||
|
|
37761a2627 | ||
|
|
968a8fde99 | ||
|
|
3043b6588f | ||
|
|
47208533a2 | ||
|
|
09ccd9c7e4 | ||
|
|
cd9dd0a312 | ||
|
|
6fbbbeccc5 | ||
|
|
c34cbf63af | ||
|
|
b76aad8d56 | ||
|
|
68bf095eae | ||
|
|
ea6e01d0e0 | ||
|
|
19038b32bd | ||
|
|
648edd71eb | ||
|
|
0c95cde712 | ||
|
|
e218431f64 | ||
|
|
d3182e0dd9 | ||
|
|
e040e875dc | ||
|
|
303da84b18 | ||
|
|
03bfdf2663 | ||
|
|
8edaef0103 | ||
|
|
fd8a8c2729 | ||
|
|
30337bf0ba | ||
|
|
5426ca6f79 | ||
|
|
a89c25700f | ||
|
|
0e0bb8ad76 | ||
|
|
1de37556d5 | ||
|
|
098685b8f3 | ||
|
|
8f549c8a8d | ||
|
|
e56d97bb28 | ||
|
|
07b72c20e9 | ||
|
|
592f251481 | ||
|
|
890c0020ed | ||
|
|
7ce0b1cba2 | ||
|
|
b8a9c37af5 | ||
|
|
1af63acae5 | ||
|
|
d68599a5f7 | ||
|
|
7ccf35b47e | ||
|
|
4f49023a08 | ||
|
|
d7ea7c358d | ||
|
|
3f0da8f6cc | ||
|
|
b7b7eff4a3 | ||
|
|
ddeb32ed33 | ||
|
|
c7be921f44 | ||
|
|
c95c3f0f7a | ||
|
|
be824ede0d | ||
|
|
cbe46cccd8 | ||
|
|
49b5157abe | ||
|
|
6f87c14bf9 | ||
|
|
0f80d16ba7 | ||
|
|
4b23b4f8ae | ||
|
|
446871d84f | ||
|
|
99bbe15980 | ||
|
|
320771551b | ||
|
|
1676d1f344 | ||
|
|
c8ae2895f0 | ||
|
|
ed1ab7fa75 | ||
|
|
73497dae9e | ||
|
|
eff775c760 | ||
|
|
e73d662686 | ||
|
|
86a6fb104a | ||
|
|
3b097dcccd | ||
|
|
e910dac0dc | ||
|
|
17a71d6011 | ||
|
|
5a6ccc8027 | ||
|
|
6dccfcb3f9 | ||
|
|
2b0c365f35 | ||
|
|
611e2f4311 | ||
|
|
561a375b48 | ||
|
|
e51345e276 | ||
|
|
00e61e18ae | ||
|
|
5490fc00d1 | ||
|
|
f1043676da | ||
|
|
f7ebcbb974 | ||
|
|
c637b963e0 | ||
|
|
9156bb80db | ||
|
|
58295ca0ab | ||
|
|
4176007493 | ||
|
|
9d7883f529 | ||
|
|
93c556acdd | ||
|
|
676b1e47e9 | ||
|
|
0b141ac08a | ||
|
|
b4db1e06a1 | ||
|
|
0d0fff6ddc | ||
|
|
6c8c26f741 | ||
|
|
f68a12558a | ||
|
|
a8a5b29fb7 | ||
|
|
037e2e54f0 | ||
|
|
ae80889477 | ||
|
|
e1acab5113 | ||
|
|
d84196db62 | ||
|
|
80c2815e67 | ||
|
|
e42a01a3dd |
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Epic/Story Issue Template
|
||||
about: Template for "top" level issues - Epics (>2 weeks) / Stories (<2 weeks)
|
||||
title: 'Epic: New Feature'
|
||||
labels: epic
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
## Summary / Background
|
||||
What do you want to achieve, why? business context
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
What will be impacted and what won't be?
|
||||
What needs implementation and what is invariant?
|
||||
e.g.
|
||||
- user should be able to run workflow X from UI
|
||||
- enable workflow Y, Z from CLI
|
||||
|
||||
## Assumptions
|
||||
Product / UX assumptions as well as technical assumptions / limitations
|
||||
e.g.
|
||||
* Support only Python Runtime
|
||||
* Focus on DVC experiments only
|
||||
* Deployment environments don't change often and can be picked up from shared configuration
|
||||
|
||||
## Open Questions
|
||||
e.g.
|
||||
- How should access control work for shared artifacts (workflow X)
|
||||
- Python runtime assumption - is it really valid? in light of <...>
|
||||
|
||||
## Blockers / Dependencies
|
||||
List issues or other conditions / blockers
|
||||
|
||||
## General Approach
|
||||
Invocation example:
|
||||
```shell
|
||||
$ mapper-run task.tar.gz --ray-cluster <ip>:<port>
|
||||
```
|
||||
|
||||
## Steps
|
||||
|
||||
### Must have (p1)
|
||||
- [ ] subissue2
|
||||
- [ ] step 2
|
||||
- info
|
||||
- info
|
||||
|
||||
### Optional / followup (p2)
|
||||
- [ ] ⌛ step 3 wip
|
||||
- [ ] step 4
|
||||
|
||||
## Timelines
|
||||
|
||||
Put your estimations here. Update once certainty changes
|
||||
- end of week (Feb 3) for prototype with workflows X, Y
|
||||
- Feb 15 - MVP in production
|
||||
- Low priority followups can be done later
|
||||
@@ -0,0 +1,9 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 2.x
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "2.x"
|
||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ["main", "2.x"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
|
||||
@@ -17,9 +17,9 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- {pkg: "deb", os: "ubuntu-18.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-18.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-10.15", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "deb", os: "ubuntu-20.04", asset: "dvc_${{ github.event.release.tag_name }}_amd64.deb"}
|
||||
- {pkg: "rpm", os: "ubuntu-20.04", asset: "dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm"}
|
||||
- {pkg: "osxpkg", os: "macos-11", asset: "dvc-${{ github.event.release.tag_name }}.pkg"}
|
||||
- {pkg: "exe", os: "windows-2019", asset: "dvc-${{ github.event.release.tag_name }}.exe"}
|
||||
|
||||
name: ${{ matrix.pkg }}
|
||||
@@ -73,6 +73,8 @@ jobs:
|
||||
|
||||
pip:
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -100,13 +102,10 @@ jobs:
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
if: ${{ github.event_name == 'release' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
||||
repository_url: https://test.pypi.org/legacy/
|
||||
skip_existing: true
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
branches: [main, 2.x]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
timeout-minutes: 45
|
||||
timeout-minutes: 50
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -41,9 +41,11 @@ jobs:
|
||||
pip install -e ".[dev]"
|
||||
- uses: pre-commit/[email protected]
|
||||
- name: run tests
|
||||
timeout-minutes: 30
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest -nauto --timeout=300 --durations=100
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
+10
-3
@@ -35,7 +35,7 @@ repos:
|
||||
args:
|
||||
- --py38-plus
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
@@ -43,11 +43,11 @@ repos:
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.238'
|
||||
rev: 'v0.0.261'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.2
|
||||
rev: v2.2.4
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
@@ -61,6 +61,13 @@ repos:
|
||||
- flake8-debugger
|
||||
- flake8-string-format
|
||||
- flake8-typing-imports
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: '1.7.5'
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml", "--quiet", "--format=custom"]
|
||||
files: ^dvc/
|
||||
additional_dependencies: [".[toml]"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ def is_enabled():
|
||||
enabled = not os.getenv(DVC_NO_ANALYTICS)
|
||||
if enabled:
|
||||
enabled = to_bool(
|
||||
Config(validate=False).get("core", {}).get("analytics", "true")
|
||||
Config.from_cwd(validate=False).get("core", {}).get("analytics", "true")
|
||||
)
|
||||
|
||||
logger.debug("Analytics is %sabled.", "en" if enabled else "dis")
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import asdict, dataclass, field, fields
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
|
||||
from funcy import compact
|
||||
from voluptuous import Required
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -26,6 +27,24 @@ class Annotation:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Artifact:
|
||||
PARAM_PATH: ClassVar[str] = "path"
|
||||
PARAM_DESC: ClassVar[str] = "desc"
|
||||
PARAM_TYPE: ClassVar[str] = "type"
|
||||
PARAM_LABELS: ClassVar[str] = "labels"
|
||||
PARAM_META: ClassVar[str] = "meta"
|
||||
|
||||
path: str
|
||||
desc: Optional[str] = None
|
||||
type: Optional[str] = None # noqa: A003
|
||||
labels: List[str] = field(default_factory=list)
|
||||
meta: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return compact(asdict(self))
|
||||
|
||||
|
||||
ANNOTATION_FIELDS = [field.name for field in fields(Annotation)]
|
||||
ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_DESC: str,
|
||||
@@ -33,3 +52,7 @@ ANNOTATION_SCHEMA = {
|
||||
Annotation.PARAM_LABELS: [str],
|
||||
Annotation.PARAM_META: object,
|
||||
}
|
||||
ARTIFACT_SCHEMA = {
|
||||
Required(Artifact.PARAM_PATH): str,
|
||||
**ANNOTATION_SCHEMA, # type: ignore[arg-type]
|
||||
}
|
||||
|
||||
+9
-2
@@ -2,14 +2,21 @@ from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .data import open # pylint: disable=redefined-builtin
|
||||
from .data import get_url, read
|
||||
from .experiments import make_checkpoint
|
||||
from .params import params_show
|
||||
from .experiments import exp_save, exp_show, make_checkpoint
|
||||
from .scm import all_branches, all_commits, all_tags
|
||||
from .show import metrics_show, params_show
|
||||
|
||||
__all__ = [
|
||||
"all_branches",
|
||||
"all_commits",
|
||||
"all_tags",
|
||||
"exp_save",
|
||||
"exp_show",
|
||||
"get_url",
|
||||
"make_checkpoint",
|
||||
"open",
|
||||
"params_show",
|
||||
"metrics_show",
|
||||
"read",
|
||||
"DVCFileSystem",
|
||||
]
|
||||
|
||||
+66
-16
@@ -1,12 +1,30 @@
|
||||
from contextlib import _GeneratorContextManager as GCM
|
||||
from typing import Optional
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from funcy import reraise
|
||||
|
||||
from dvc.exceptions import OutputNotFoundError, PathMissingError
|
||||
from dvc.exceptions import FileMissingError, OutputNotFoundError, PathMissingError
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _wrap_exceptions(repo, url):
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputInExternalRepoError, NoRemoteInExternalRepoError
|
||||
|
||||
try:
|
||||
yield
|
||||
except NoRemoteError as exc:
|
||||
raise NoRemoteInExternalRepoError(url) from exc
|
||||
except OutputNotFoundError as exc:
|
||||
if exc.repo is repo:
|
||||
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url) from exc
|
||||
raise
|
||||
except FileMissingError as exc:
|
||||
raise PathMissingError(exc.path, url) from exc
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
@@ -19,18 +37,20 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
info = _repo.dvcfs.info(fs_path)
|
||||
with _wrap_exceptions(_repo, path):
|
||||
fs_path = _repo.dvcfs.from_os_path(path)
|
||||
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
with reraise(FileNotFoundError, PathMissingError(path, repo)):
|
||||
info = _repo.dvcfs.info(fs_path)
|
||||
|
||||
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
|
||||
md5 = dvc_info["md5"]
|
||||
dvc_info = info.get("dvc_info")
|
||||
if not dvc_info:
|
||||
raise OutputNotFoundError(path, repo)
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
dvc_repo = info["repo"] # pylint: disable=unsubscriptable-object
|
||||
md5 = dvc_info["md5"]
|
||||
|
||||
return dvc_repo.cloud.get_url_for(remote, checksum=md5)
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
@@ -194,11 +214,41 @@ def open( # noqa, pylint: disable=redefined-builtin
|
||||
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
with Repo.open(repo, rev=rev, subrepos=True, uninitialized=True) as _repo:
|
||||
with _repo.open_by_relpath(
|
||||
path, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
yield fd
|
||||
repo_kwargs: Dict[str, Any] = {"subrepos": True, "uninitialized": True}
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
|
||||
with Repo.open(repo, rev=rev, **repo_kwargs) as _repo:
|
||||
with _wrap_exceptions(_repo, path):
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||
fs_path = path
|
||||
else:
|
||||
fs = DVCFileSystem(repo=_repo, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
|
||||
try:
|
||||
with fs.open(
|
||||
fs_path,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError as exc:
|
||||
raise FileMissingError(path) from exc
|
||||
except IsADirectoryError as exc:
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import builtins
|
||||
import os
|
||||
from time import sleep
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from dvc.env import DVC_CHECKPOINT, DVC_ROOT
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
from dvc.stage.monitor import CheckpointTask
|
||||
|
||||
|
||||
@@ -31,3 +35,114 @@ def make_checkpoint():
|
||||
os.fsync(fobj.fileno())
|
||||
while os.path.exists(signal_file):
|
||||
sleep(0.1)
|
||||
|
||||
|
||||
def exp_save(
|
||||
name: Optional[str] = None,
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Create a new DVC experiment using `exp save`.
|
||||
|
||||
See https://dvc.org/doc/command-reference/exp/save.
|
||||
|
||||
Args:
|
||||
name (str, optional): specify a name for this experiment.
|
||||
If `None`, a default one will be generated, such as `urban-sign`.
|
||||
Defaults to `None`.
|
||||
force (bool): overwrite the experiment if an experiment with the same
|
||||
name already exists.
|
||||
Defaults to `False`.
|
||||
include_untracked (List[str], optional): specify untracked file(s) to
|
||||
be included in the saved experiment.
|
||||
Defaults to `None`.
|
||||
|
||||
Returns:
|
||||
str: The `Git revision`_ of the created experiment.
|
||||
|
||||
Raises:
|
||||
ExperimentExistsError: If an experiment with `name` already exists and
|
||||
`force=False`.
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
with Repo() as repo:
|
||||
return repo.experiments.save(
|
||||
name=name, force=force, include_untracked=include_untracked
|
||||
)
|
||||
|
||||
|
||||
def _postprocess(exp_rows):
|
||||
for exp_row in exp_rows:
|
||||
for k, v in exp_row.items():
|
||||
if isinstance(v, Text):
|
||||
v_str = str(v)
|
||||
try:
|
||||
exp_row[k] = float(v_str)
|
||||
except ValueError:
|
||||
exp_row[k] = v_str
|
||||
|
||||
if not exp_row[k]:
|
||||
exp_row[k] = None
|
||||
|
||||
return exp_rows
|
||||
|
||||
|
||||
def exp_show(
|
||||
repo: Optional[str] = None,
|
||||
revs: Optional[Union[str, List[str]]] = None,
|
||||
num: int = 1,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
) -> List[Dict]:
|
||||
"""Get DVC experiments tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all experiments derived from
|
||||
the Git `HEAD`.
|
||||
|
||||
See the options below to customize the experiments retrieved.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
revs (Union[str, List[str]], optional): Git revision(s) (e.g. branch,
|
||||
tag, SHA commit) to use as a reference point to start listing
|
||||
experiments.
|
||||
Defaults to `None`, which will use `HEAD` as starting point.
|
||||
num (int, optional): show experiments from the last `num` commits
|
||||
(first parents) starting from the `revs` baseline.
|
||||
Give a negative value to include all first-parent commits (similar
|
||||
to `git log -n`).
|
||||
Defaults to 1.
|
||||
param_deps (bool, optional): include only parameters that are stage
|
||||
dependencies.
|
||||
Defaults to `False`.
|
||||
force (bool, optional): force re-collection of experiments instead of
|
||||
loading from internal experiments cache.
|
||||
DVC caches `exp_show` data for completed experiments to improve
|
||||
performance of subsequent calls.
|
||||
When `force` is specified, DVC will reload all experiment data and
|
||||
ignore any previously cached results.
|
||||
Defaults to `False`.
|
||||
|
||||
Returns:
|
||||
List[Dict]: Each item in the list will contain a dictionary with
|
||||
the info for an individual experiment.
|
||||
See Examples below.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
experiments = _repo.experiments.show(
|
||||
revs=revs,
|
||||
num=num,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
)
|
||||
td, _ = tabulate(experiments, fill_value=None)
|
||||
|
||||
return _postprocess(td.as_dict())
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def all_branches(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git branches in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: Names of the Git branches.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_branches()
|
||||
|
||||
|
||||
def all_commits(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git commits in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: SHAs of the Git commits.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_all_commits()
|
||||
|
||||
|
||||
def all_tags(repo: Optional[str] = None) -> List[str]:
|
||||
"""Get all Git tags in a DVC repository.
|
||||
|
||||
Args:
|
||||
repo (str, optional): location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
Returns:
|
||||
List[str]: Names of the Git tags.
|
||||
"""
|
||||
with Repo.open(repo) as _repo:
|
||||
return _repo.scm.list_tags()
|
||||
@@ -4,10 +4,153 @@ from typing import Dict, Iterable, Optional, Union
|
||||
|
||||
from funcy import first
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def _onerror_raise(exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
|
||||
def _postprocess(results):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in results.items():
|
||||
if not rev_data:
|
||||
continue
|
||||
|
||||
processed[rev] = {}
|
||||
|
||||
counts: typing.Counter[str] = Counter()
|
||||
for file_data in rev_data["data"].values():
|
||||
for k in file_data["data"]:
|
||||
counts[k] += 1
|
||||
|
||||
for file_name, file_data in rev_data["data"].items():
|
||||
to_merge = {
|
||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||
for k, v in file_data["data"].items()
|
||||
}
|
||||
processed[rev] = {**processed[rev], **to_merge}
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
|
||||
return processed
|
||||
|
||||
|
||||
def metrics_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
) -> Dict:
|
||||
"""Get metrics tracked in `repo`.
|
||||
|
||||
Without arguments, this function will retrieve all metrics from all tracked
|
||||
metric files, for the current working tree.
|
||||
|
||||
See the options below to restrict the metrics retrieved.
|
||||
|
||||
Args:
|
||||
*targets (str, optional): Names of the metric files to retrieve
|
||||
metrics from. For example, "classifier_eval.json,
|
||||
clustering_eval.json".
|
||||
If no `targets` are provided, all metric files tracked in `dvc.yaml`
|
||||
will be used.
|
||||
Note that targets don't necessarily have to be defined in `dvc.yaml`.
|
||||
repo (str, optional): Location of the DVC repository.
|
||||
Defaults to the current project (found by walking up from the
|
||||
current working directory tree).
|
||||
It can be a URL or a file system path.
|
||||
Both HTTP and SSH protocols are supported for online Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
rev (str, optional): Name of the `Git revision`_ to retrieve metrics
|
||||
from.
|
||||
Defaults to `None`.
|
||||
An example of git revision can be a branch or tag name, a commit
|
||||
hash or a dvc experiment name.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
If `None`, the current working tree will be used.
|
||||
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show()
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9249974999612706,
|
||||
"roc_auc": 0.9460213440787918
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `rev`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show(rev="tune-hyperparams")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9268792615819422,
|
||||
"roc_auc": 0.945093365854111
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Using `targets`.
|
||||
|
||||
Working on https://github.com/iterative/example-get-started
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show("evaluation.json")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"avg_prec": 0.9249974999612706,
|
||||
"roc_auc": 0.9460213440787918
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
- Git URL as `repo`.
|
||||
|
||||
>>> import json
|
||||
>>> import dvc.api
|
||||
>>> metrics = dvc.api.metrics_show(
|
||||
... repo="https://github.com/iterative/demo-fashion-mnist")
|
||||
>>> print(json.dumps(metrics, indent=4))
|
||||
{
|
||||
"loss": 0.25284987688064575,
|
||||
"accuracy": 0.9071000218391418
|
||||
}
|
||||
|
||||
|
||||
.. _Git revision:
|
||||
https://git-scm.com/docs/revisions
|
||||
"""
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
metrics = _repo.metrics.show(
|
||||
targets=targets,
|
||||
revs=rev if rev is None else [rev],
|
||||
onerror=_onerror_raise,
|
||||
)
|
||||
|
||||
metrics = _postprocess(metrics)
|
||||
|
||||
if not metrics:
|
||||
return {}
|
||||
|
||||
return metrics[first(metrics)]
|
||||
|
||||
|
||||
def params_show(
|
||||
*targets: str,
|
||||
repo: Optional[str] = None,
|
||||
@@ -56,9 +199,6 @@ def params_show(
|
||||
Returns:
|
||||
Dict: See Examples below.
|
||||
|
||||
Raises:
|
||||
DvcException: If no params are found in `repo`.
|
||||
|
||||
Examples:
|
||||
|
||||
- No arguments.
|
||||
@@ -239,34 +379,6 @@ def params_show(
|
||||
if isinstance(stages, str):
|
||||
stages = [stages]
|
||||
|
||||
def _onerror_raise(_result: Dict, exception: Exception, *args, **kwargs):
|
||||
raise exception
|
||||
|
||||
def _postprocess(params):
|
||||
processed: Dict[str, Dict] = {}
|
||||
for rev, rev_data in params.items():
|
||||
processed[rev] = {}
|
||||
|
||||
counts: typing.Counter[str] = Counter()
|
||||
for file_data in rev_data["data"].values():
|
||||
for k in file_data["data"]:
|
||||
counts[k] += 1
|
||||
|
||||
for file_name, file_data in rev_data["data"].items():
|
||||
to_merge = {
|
||||
(k if counts[k] == 1 else f"{file_name}:{k}"): v
|
||||
for k, v in file_data["data"].items()
|
||||
}
|
||||
processed[rev] = {**processed[rev], **to_merge}
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
|
||||
if not processed:
|
||||
raise DvcException("No params found")
|
||||
|
||||
return processed[first(processed)]
|
||||
|
||||
with Repo.open(repo) as _repo:
|
||||
params = _repo.params.show(
|
||||
revs=rev if rev is None else [rev],
|
||||
@@ -276,4 +388,9 @@ def params_show(
|
||||
stages=stages,
|
||||
)
|
||||
|
||||
return _postprocess(params)
|
||||
params = _postprocess(params)
|
||||
|
||||
if not params:
|
||||
return {}
|
||||
|
||||
return params[first(params)]
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
|
||||
from dvc.fs import GitFileSystem, Schemes
|
||||
from dvc_data.hashfile.db import get_odb
|
||||
|
||||
@@ -9,12 +11,11 @@ def _get_odb(repo, settings, fs=None):
|
||||
return None
|
||||
|
||||
cls, config, fs_path = get_cloud_fs(repo, **settings)
|
||||
config["tmp_dir"] = repo.tmp_dir
|
||||
fs = fs or cls(**config)
|
||||
return get_odb(fs, fs_path, state=repo.state, **config)
|
||||
|
||||
|
||||
class ODBManager:
|
||||
class CacheManager:
|
||||
CACHE_DIR = "cache"
|
||||
CLOUD_SCHEMES = [
|
||||
Schemes.S3,
|
||||
@@ -29,16 +30,21 @@ class ODBManager:
|
||||
self.config = config = repo.config["cache"]
|
||||
self._odb = {}
|
||||
|
||||
default = None
|
||||
if repo and repo.local_dvc_dir:
|
||||
default = os.path.join(repo.local_dvc_dir, self.CACHE_DIR)
|
||||
|
||||
local = config.get("local")
|
||||
|
||||
if local:
|
||||
settings = {"name": local}
|
||||
elif "dir" not in config:
|
||||
elif "dir" not in config and not default:
|
||||
settings = None
|
||||
else:
|
||||
from dvc.config_schema import LOCAL_COMMON
|
||||
|
||||
settings = {"url": config["dir"]}
|
||||
url = config.get("dir") or default
|
||||
settings = {"url": url}
|
||||
for opt in LOCAL_COMMON:
|
||||
if opt in config:
|
||||
settings[str(opt)] = config.get(opt)
|
||||
+12
-8
@@ -39,12 +39,17 @@ def parse_args(argv=None):
|
||||
|
||||
def _log_unknown_exceptions() -> None:
|
||||
from dvc.info import get_dvc_info
|
||||
from dvc.logger import FOOTER
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
logger.exception("unexpected error")
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Version info for developers:\n%s", get_dvc_info())
|
||||
logger.info(FOOTER)
|
||||
|
||||
q = colorize("Having any troubles?", "yellow")
|
||||
link = colorize("https://dvc.org/support", "blue")
|
||||
footer = f"\n{q} Hit us up at {link}, we are always happy to help!"
|
||||
ui.error_write(footer)
|
||||
|
||||
|
||||
def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
@@ -141,7 +146,7 @@ def _log_exceptions(exc: Exception) -> Optional[int]:
|
||||
return None
|
||||
|
||||
|
||||
def main(argv=None): # noqa: C901
|
||||
def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
"""Main entry point for dvc CLI.
|
||||
|
||||
Args:
|
||||
@@ -153,7 +158,7 @@ def main(argv=None): # noqa: C901
|
||||
from dvc._debug import debugtools
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.logger import disable_other_loggers, set_loggers_level
|
||||
from dvc.logger import set_loggers_level
|
||||
|
||||
# NOTE: stderr/stdout may be closed if we are running from dvc.daemon.
|
||||
# On Linux we directly call cli.main after double forking and closing
|
||||
@@ -166,9 +171,8 @@ def main(argv=None): # noqa: C901
|
||||
logging.disable(logging.INFO)
|
||||
|
||||
args = None
|
||||
disable_other_loggers()
|
||||
|
||||
outerLogLevel = logger.level
|
||||
outer_log_level = logger.level
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
@@ -239,9 +243,9 @@ def main(argv=None): # noqa: C901
|
||||
|
||||
return ret
|
||||
finally:
|
||||
logger.setLevel(outerLogLevel)
|
||||
logger.setLevel(outer_log_level)
|
||||
|
||||
from dvc.external_repo import clean_repos
|
||||
from dvc.repo.open_repo import clean_repos
|
||||
|
||||
# Remove cached repos in the end of the call, these are anonymous
|
||||
# so won't be reused by any other subsequent run anyway.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
from functools import lru_cache
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.commands import (
|
||||
@@ -150,6 +151,7 @@ def get_parent_parser():
|
||||
return parent_parser
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_main_parser():
|
||||
parent_parser = get_parent_parser()
|
||||
|
||||
|
||||
+11
-1
@@ -62,8 +62,11 @@ class CmdAdd(CmdBase):
|
||||
remote=self.args.remote,
|
||||
to_remote=self.args.to_remote,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -140,6 +143,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
|
||||
_add_annotating_args(parser)
|
||||
parser.add_argument(
|
||||
|
||||
+11
-2
@@ -1,4 +1,5 @@
|
||||
import argparse
|
||||
import os
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.utils import append_doc_link, fix_subparsers
|
||||
@@ -9,13 +10,21 @@ from dvc.ui import ui
|
||||
class CmdCacheDir(CmdConfig):
|
||||
def run(self):
|
||||
if self.args.value is None and not self.args.unset:
|
||||
from dvc.config import ConfigError
|
||||
|
||||
if self.args.level:
|
||||
conf = self.config.read(level=self.args.level)
|
||||
else:
|
||||
# Use merged config with default values
|
||||
conf = self.config
|
||||
self._check(conf, False, "cache", "dir")
|
||||
ui.write(conf["cache"]["dir"])
|
||||
try:
|
||||
self._check(conf, False, "cache", "dir")
|
||||
path = conf["cache"]["dir"]
|
||||
except ConfigError:
|
||||
if not self.config.dvc_dir or self.args.level:
|
||||
raise
|
||||
path = os.path.join(self.config.dvc_dir, "cache")
|
||||
ui.write(path)
|
||||
return 0
|
||||
with self.config.edit(level=self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
|
||||
@@ -39,7 +39,7 @@ class CmdCheckIgnore(CmdBase):
|
||||
target = input()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
break
|
||||
if target == "":
|
||||
if not target:
|
||||
break
|
||||
if not self._check_one_file(target):
|
||||
ret = 0
|
||||
|
||||
@@ -15,7 +15,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=PREAMBLE) # nosec B604
|
||||
ui.write(script, force=True)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
super().__init__(args)
|
||||
|
||||
self.config = Config(validate=False)
|
||||
self.config = Config.from_cwd(validate=False)
|
||||
|
||||
def run(self):
|
||||
if self.args.show_origin and (self.args.value or self.args.unset):
|
||||
|
||||
+23
-24
@@ -9,11 +9,11 @@ if TYPE_CHECKING:
|
||||
from networkx import DiGraph
|
||||
|
||||
|
||||
def _show_ascii(G: "DiGraph"):
|
||||
def _show_ascii(graph: "DiGraph"):
|
||||
from dvc.dagascii import draw
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
pipelines = get_pipelines(graph)
|
||||
|
||||
ret = []
|
||||
for pipeline in pipelines:
|
||||
@@ -31,7 +31,7 @@ def _quote_label(node):
|
||||
return label
|
||||
|
||||
|
||||
def _show_dot(G: "DiGraph"):
|
||||
def _show_dot(graph: "DiGraph"):
|
||||
import io
|
||||
|
||||
import networkx as nx
|
||||
@@ -39,15 +39,15 @@ def _show_dot(G: "DiGraph"):
|
||||
|
||||
dot_file = io.StringIO()
|
||||
|
||||
nx.relabel_nodes(G, _quote_label, copy=False)
|
||||
write_dot(G.reverse(), dot_file)
|
||||
nx.relabel_nodes(graph, _quote_label, copy=False)
|
||||
write_dot(graph.reverse(), dot_file)
|
||||
return dot_file.getvalue()
|
||||
|
||||
|
||||
def _show_mermaid(G, markdown: bool = False):
|
||||
def _show_mermaid(graph, markdown: bool = False):
|
||||
from dvc.repo.graph import get_pipelines
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
pipelines = get_pipelines(graph)
|
||||
|
||||
graph = "flowchart TD"
|
||||
|
||||
@@ -100,43 +100,42 @@ def _transform(index, outs):
|
||||
def _relabel(node) -> str:
|
||||
return node.addressing if isinstance(node, Stage) else str(node)
|
||||
|
||||
G = index.outs_graph if outs else index.graph
|
||||
return nx.relabel_nodes(G, _relabel, copy=True)
|
||||
graph = index.outs_graph if outs else index.graph
|
||||
return nx.relabel_nodes(graph, _relabel, copy=True)
|
||||
|
||||
|
||||
def _filter(G, targets, full):
|
||||
def _filter(graph, targets, full):
|
||||
import networkx as nx
|
||||
|
||||
if not targets:
|
||||
return G
|
||||
return graph
|
||||
|
||||
H = G.copy()
|
||||
new_graph = graph.copy()
|
||||
if not full:
|
||||
descendants = set()
|
||||
for target in targets:
|
||||
descendants.update(nx.descendants(G, target))
|
||||
descendants.update(nx.descendants(graph, target))
|
||||
descendants.add(target)
|
||||
H.remove_nodes_from(set(G.nodes()) - descendants)
|
||||
new_graph.remove_nodes_from(set(graph.nodes()) - descendants)
|
||||
|
||||
undirected = H.to_undirected()
|
||||
undirected = new_graph.to_undirected()
|
||||
connected = set()
|
||||
for target in targets:
|
||||
connected.update(nx.node_connected_component(undirected, target))
|
||||
|
||||
H.remove_nodes_from(set(H.nodes()) - connected)
|
||||
|
||||
return H
|
||||
new_graph.remove_nodes_from(set(new_graph.nodes()) - connected)
|
||||
return new_graph
|
||||
|
||||
|
||||
def _build(repo, target=None, full=False, outs=False):
|
||||
targets = _collect_targets(repo, target, outs)
|
||||
G = _transform(repo.index, outs)
|
||||
return _filter(G, targets, full)
|
||||
graph = _transform(repo.index, outs)
|
||||
return _filter(graph, targets, full)
|
||||
|
||||
|
||||
class CmdDAG(CmdBase):
|
||||
def run(self):
|
||||
G = _build(
|
||||
graph = _build(
|
||||
self.repo,
|
||||
target=self.args.target,
|
||||
full=self.args.full,
|
||||
@@ -144,12 +143,12 @@ class CmdDAG(CmdBase):
|
||||
)
|
||||
|
||||
if self.args.dot:
|
||||
ui.write(_show_dot(G))
|
||||
ui.write(_show_dot(graph))
|
||||
elif self.args.mermaid or self.args.markdown:
|
||||
ui.write(_show_mermaid(G, self.args.markdown))
|
||||
ui.write(_show_mermaid(graph, self.args.markdown))
|
||||
else:
|
||||
with ui.pager():
|
||||
ui.write(_show_ascii(G))
|
||||
ui.write(_show_ascii(graph))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class CmdDataStatus(CmdBase):
|
||||
COLORS = {
|
||||
"not_in_remote": "red",
|
||||
"not_in_cache": "red",
|
||||
"committed": "green",
|
||||
"uncommitted": "yellow",
|
||||
"untracked": "cyan",
|
||||
}
|
||||
LABELS = {
|
||||
"not_in_remote": "Not in remote",
|
||||
"not_in_cache": "Not in cache",
|
||||
"committed": "DVC committed changes",
|
||||
"uncommitted": "DVC uncommitted changes",
|
||||
@@ -34,6 +36,7 @@ class CmdDataStatus(CmdBase):
|
||||
"unchanged": "DVC unchanged files",
|
||||
}
|
||||
HINTS = {
|
||||
"not_in_remote": ('use "dvc push <file>..." to upload files',),
|
||||
"not_in_cache": ('use "dvc fetch <file>..." to download files',),
|
||||
"committed": ("git commit the corresponding dvc files to update the repo",),
|
||||
"uncommitted": (
|
||||
@@ -114,6 +117,8 @@ class CmdDataStatus(CmdBase):
|
||||
status = self.repo.data_status(
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
|
||||
if not self.args.unchanged:
|
||||
@@ -239,6 +244,18 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show files not in remote.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"--no-remote-refresh",
|
||||
dest="remote_refresh",
|
||||
action="store_false",
|
||||
help="Use cached remote index (don't check remote).",
|
||||
)
|
||||
data_status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
DATA_LS_HELP = "List data tracked by DVC with its metadata."
|
||||
|
||||
@@ -40,6 +40,7 @@ class CmdDataPull(CmdDataBase):
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
glob=self.args.glob,
|
||||
allow_missing=self.args.allow_missing,
|
||||
)
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
@@ -197,6 +198,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Ignore errors if some of the files or directories are missing.",
|
||||
)
|
||||
pull_parser.set_defaults(func=CmdDataPull)
|
||||
|
||||
# Push
|
||||
|
||||
@@ -4,6 +4,7 @@ from dvc.cli.utils import append_doc_link, fix_subparsers, hide_subparsers_from_
|
||||
from dvc.commands.experiments import (
|
||||
apply,
|
||||
branch,
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
@@ -21,6 +22,7 @@ from dvc.commands.experiments import (
|
||||
SUB_COMMANDS = [
|
||||
apply,
|
||||
branch,
|
||||
clean,
|
||||
diff,
|
||||
exec_run,
|
||||
gc,
|
||||
@@ -80,6 +82,7 @@ def add_rev_selection_flags(
|
||||
experiments_subcmd_parser.add_argument(
|
||||
"--rev",
|
||||
type=str,
|
||||
action="append",
|
||||
default=None,
|
||||
help=msg,
|
||||
metavar="<commit>",
|
||||
|
||||
@@ -4,13 +4,21 @@ import logging
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsApply(CmdBase):
|
||||
def run(self):
|
||||
self.repo.experiments.apply(self.args.experiment, force=self.args.force)
|
||||
if not self.args.force:
|
||||
ui.write(
|
||||
"The --no-force option is deprecated and will be removed in a future"
|
||||
" DVC release. To revert the result of 'exp apply', run:\n"
|
||||
"\n\tgit reset --hard\n"
|
||||
"\tgit stash apply refs/exps/apply/stash\n"
|
||||
)
|
||||
self.repo.experiments.apply(self.args.experiment)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -26,5 +26,13 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
experiments_branch_parser.add_argument(
|
||||
"experiment", help="Experiment to be promoted."
|
||||
)
|
||||
experiments_branch_parser.add_argument("branch", help="Name for the new Git branch")
|
||||
experiments_branch_parser.add_argument(
|
||||
"branch",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help=(
|
||||
"Optional name for the new Git branch. "
|
||||
"Defaults to '{experiment-name}-branch'."
|
||||
),
|
||||
)
|
||||
experiments_branch_parser.set_defaults(func=CmdExperimentsBranch)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdExperimentsClean(CmdBase):
|
||||
def run(self):
|
||||
self.repo.experiments.clean()
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_CLEAN_HELP = "Cleanup experiments temporary internal files."
|
||||
experiments_clean_parser = experiments_subparsers.add_parser(
|
||||
"clean",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(EXPERIMENTS_CLEAN_HELP, "exp/clean"),
|
||||
help=EXPERIMENTS_CLEAN_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
experiments_clean_parser.set_defaults(func=CmdExperimentsClean)
|
||||
@@ -52,7 +52,7 @@ class CmdExperimentsDiff(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments in the DVC repository."
|
||||
EXPERIMENTS_DIFF_HELP = "Show changes between experiments."
|
||||
|
||||
experiments_diff_parser = experiments_subparsers.add_parser(
|
||||
"diff",
|
||||
|
||||
@@ -19,6 +19,8 @@ class CmdExecutorRun(CmdBaseNoRepo):
|
||||
queue=None,
|
||||
log_level=logger.getEffectiveLevel(),
|
||||
infofile=self.args.infofile,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -36,4 +38,21 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help="Path to executor info file",
|
||||
default=None,
|
||||
)
|
||||
exec_run_parser.add_argument(
|
||||
"-C",
|
||||
"--copy-paths",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"List of ignored or untracked paths to copy into the temp directory."
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
exec_run_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
exec_run_parser.set_defaults(func=CmdExecutorRun)
|
||||
|
||||
@@ -78,7 +78,8 @@ class CmdExperimentsGC(CmdBase):
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_GC_HELP = "Garbage collect unneeded experiments."
|
||||
EXPERIMENTS_GC_DESCRIPTION = (
|
||||
"Removes all experiments which are not derived from the specifiedGit revisions."
|
||||
"Removes all experiments which are not derived"
|
||||
" from the specified Git revisions."
|
||||
)
|
||||
experiments_gc_parser = experiments_subparsers.add_parser(
|
||||
"gc",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
@@ -18,29 +19,66 @@ class CmdExperimentsPush(CmdBase):
|
||||
"`--rev` or `--all-commits` flag."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_result(result: Dict[str, Any], remote: str):
|
||||
from dvc.utils import humanize
|
||||
|
||||
def join_exps(exps):
|
||||
return humanize.join([f"[bold]{e}[/]" for e in exps])
|
||||
|
||||
if diverged_exps := result.get("diverged"):
|
||||
exps = join_exps(diverged_exps)
|
||||
ui.error_write(
|
||||
f"[yellow]Local experiment {exps} has diverged "
|
||||
"from remote experiment with the same name.\n"
|
||||
"To override the remote experiment re-run with '--force'.",
|
||||
styled=True,
|
||||
)
|
||||
if uptodate_exps := result.get("up_to_date"):
|
||||
exps = join_exps(uptodate_exps)
|
||||
verb = "are" if len(uptodate_exps) > 1 else "is"
|
||||
ui.write(
|
||||
f"Experiment {exps} {verb} up to date on Git remote {remote!r}.",
|
||||
styled=True,
|
||||
)
|
||||
if pushed_exps := result.get("success"):
|
||||
exps = join_exps(pushed_exps)
|
||||
ui.write(f"Pushed experiment {exps} to Git remote {remote!r}.", styled=True)
|
||||
if not uptodate_exps and not pushed_exps:
|
||||
ui.write("No experiments to push.")
|
||||
|
||||
if uploaded := result.get("uploaded"):
|
||||
stats = {"uploaded": uploaded}
|
||||
ui.write(humanize.get_summary(stats.items()))
|
||||
|
||||
if project_url := result.get("url"):
|
||||
ui.rich_print(
|
||||
"View your experiments at", project_url, style="yellow", soft_wrap=True
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.repo.experiments.push import UploadError
|
||||
|
||||
self.raise_error_if_all_disabled()
|
||||
|
||||
pushed_exps = self.repo.experiments.push(
|
||||
self.args.git_remote,
|
||||
self.args.experiment,
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
force=self.args.force,
|
||||
push_cache=self.args.push_cache,
|
||||
dvc_remote=self.args.dvc_remote,
|
||||
jobs=self.args.jobs,
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
|
||||
if pushed_exps:
|
||||
ui.write(
|
||||
f"Pushed experiment '{pushed_exps}'"
|
||||
f"to Git remote '{self.args.git_remote}'."
|
||||
try:
|
||||
result = self.repo.experiments.push(
|
||||
self.args.git_remote,
|
||||
self.args.experiment,
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
force=self.args.force,
|
||||
push_cache=self.args.push_cache,
|
||||
dvc_remote=self.args.dvc_remote,
|
||||
jobs=self.args.jobs,
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
else:
|
||||
ui.write("No experiments to push.")
|
||||
except UploadError as e:
|
||||
self.log_result(e.result, self.args.git_remote)
|
||||
raise
|
||||
|
||||
self.log_result(result, self.args.git_remote)
|
||||
if not self.args.push_cache:
|
||||
ui.write(
|
||||
"To push cached outputs",
|
||||
|
||||
@@ -9,7 +9,9 @@ class CmdQueueWorker(CmdBase):
|
||||
"""Run the exp queue worker."""
|
||||
|
||||
def run(self):
|
||||
self.repo.experiments.celery_queue.worker.start(self.args.name)
|
||||
self.repo.experiments.celery_queue.worker.start(
|
||||
self.args.name, fsapp_clean=self.args.clean
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -22,4 +24,9 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
add_help=False,
|
||||
)
|
||||
parser.add_argument("name", help="Celery worker name.")
|
||||
parser.add_argument(
|
||||
"--clean",
|
||||
action="store_true",
|
||||
help="Automatically cleanup celery broker on shutdown.",
|
||||
)
|
||||
parser.set_defaults(func=CmdQueueWorker)
|
||||
|
||||
@@ -24,9 +24,11 @@ class CmdExperimentsRemove(CmdBase):
|
||||
)
|
||||
|
||||
def run(self):
|
||||
from dvc.utils import humanize
|
||||
|
||||
self.check_arguments()
|
||||
|
||||
removed_list = self.repo.experiments.remove(
|
||||
removed = self.repo.experiments.remove(
|
||||
exp_names=self.args.experiment,
|
||||
all_commits=self.args.all_commits,
|
||||
rev=self.args.rev,
|
||||
@@ -34,8 +36,10 @@ class CmdExperimentsRemove(CmdBase):
|
||||
queue=self.args.queue,
|
||||
git_remote=self.args.git_remote,
|
||||
)
|
||||
removed = ",".join(removed_list)
|
||||
ui.write(f"Removed experiments: {removed}")
|
||||
if removed:
|
||||
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
|
||||
else:
|
||||
ui.write("No experiments to remove.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ class CmdExperimentsRun(CmdRepro):
|
||||
reset=self.args.reset,
|
||||
tmp_dir=self.args.tmp_dir,
|
||||
machine=self.args.machine,
|
||||
copy_paths=self.args.copy_paths,
|
||||
message=self.args.message,
|
||||
**self._common_kwargs,
|
||||
)
|
||||
|
||||
@@ -136,3 +138,20 @@ def _add_run_common(parser):
|
||||
# )
|
||||
# metavar="<name>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-C",
|
||||
"--copy-paths",
|
||||
action="append",
|
||||
default=[],
|
||||
help=(
|
||||
"List of ignored or untracked paths to copy into the temp directory."
|
||||
" Only used if `--temp` or `--queue` is specified."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
name=self.args.name,
|
||||
force=self.args.force,
|
||||
include_untracked=self.args.include_untracked,
|
||||
message=self.args.message,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to save experiment")
|
||||
@@ -35,7 +36,7 @@ class CmdExperimentsSave(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as a dvc experiment."
|
||||
EXPERIMENTS_SAVE_HELP = "Save current workspace as an experiment."
|
||||
save_parser = experiments_subparsers.add_parser(
|
||||
"save",
|
||||
parents=[parent_parser],
|
||||
@@ -75,4 +76,11 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
help="List of untracked paths to include in the experiment.",
|
||||
metavar="<path>",
|
||||
)
|
||||
save_parser.add_argument(
|
||||
"-M",
|
||||
"--message",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Custom commit message to use when committing the experiment.",
|
||||
)
|
||||
save_parser.set_defaults(func=CmdExperimentsSave)
|
||||
|
||||
@@ -2,9 +2,8 @@ import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING, Any, Dict, Set
|
||||
from typing import TYPE_CHECKING, Dict, Iterable
|
||||
|
||||
from funcy import lmap
|
||||
|
||||
@@ -12,9 +11,8 @@ from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.metrics import DEFAULT_PRECISION
|
||||
from dvc.exceptions import DvcException, InvalidArgumentError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.flatten import flatten
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,30 +26,6 @@ FILL_VALUE_ERRORED = "!"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _update_names(names, items):
|
||||
for name, item in items:
|
||||
item = item.get("data", {})
|
||||
if isinstance(item, dict):
|
||||
item = flatten(item)
|
||||
names[name].update({key: None for key in item})
|
||||
|
||||
|
||||
def _collect_names(all_experiments, **kwargs):
|
||||
metric_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
||||
param_names: Dict[str, Dict[str, Any]] = defaultdict(dict)
|
||||
deps_names: Set[str] = set()
|
||||
|
||||
for _, experiments in all_experiments.items():
|
||||
for exp_data in experiments.values():
|
||||
exp = exp_data.get("data", {})
|
||||
_update_names(metric_names, exp.get("metrics", {}).items())
|
||||
_update_names(param_names, exp.get("params", {}).items())
|
||||
for dep_name in exp.get("deps", {}):
|
||||
deps_names.add(dep_name)
|
||||
|
||||
return metric_names, param_names, sorted(deps_names)
|
||||
|
||||
|
||||
experiment_types = {
|
||||
"checkpoint_tip": "│ ╓",
|
||||
"checkpoint_commit": "│ ╟",
|
||||
@@ -62,229 +36,6 @@ experiment_types = {
|
||||
}
|
||||
|
||||
|
||||
def _collect_rows( # noqa: C901
|
||||
base_rev,
|
||||
experiments,
|
||||
all_headers,
|
||||
metric_headers,
|
||||
param_headers,
|
||||
metric_names,
|
||||
param_names,
|
||||
deps_names,
|
||||
precision=DEFAULT_PRECISION,
|
||||
sort_by=None,
|
||||
sort_order=None,
|
||||
fill_value=FILL_VALUE,
|
||||
iso=False,
|
||||
):
|
||||
from dvc.scm import Git
|
||||
|
||||
if sort_by:
|
||||
sort_path, sort_name, sort_type = _sort_column(
|
||||
sort_by, metric_names, param_names
|
||||
)
|
||||
reverse = sort_order == "desc"
|
||||
experiments = _sort_exp(experiments, sort_path, sort_name, sort_type, reverse)
|
||||
|
||||
new_checkpoint = True
|
||||
for i, (rev, results) in enumerate(experiments.items()):
|
||||
fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
|
||||
row_dict = {k: fill_value for k in all_headers}
|
||||
|
||||
exp = results.get("data", {})
|
||||
|
||||
state = exp.get("status")
|
||||
if state == "Success":
|
||||
state = fill_value
|
||||
|
||||
is_baseline = rev == "baseline"
|
||||
|
||||
if is_baseline:
|
||||
name_rev = base_rev[:7] if Git.is_sha(base_rev) else base_rev
|
||||
else:
|
||||
name_rev = rev[:7]
|
||||
|
||||
tip = exp.get("checkpoint_tip")
|
||||
parent_rev = exp.get("checkpoint_parent", "")
|
||||
parent_exp = experiments.get(parent_rev, {}).get("data", {})
|
||||
parent_tip = parent_exp.get("checkpoint_tip")
|
||||
|
||||
parent = ""
|
||||
if is_baseline:
|
||||
typ = "baseline"
|
||||
elif tip:
|
||||
if tip == parent_tip:
|
||||
typ = "checkpoint_tip" if new_checkpoint else "checkpoint_commit"
|
||||
elif parent_rev == base_rev:
|
||||
typ = "checkpoint_base"
|
||||
else:
|
||||
typ = "checkpoint_commit"
|
||||
parent = parent_rev[:7]
|
||||
elif i < len(experiments) - 1:
|
||||
typ = "branch_commit"
|
||||
else:
|
||||
typ = "branch_base"
|
||||
|
||||
if not is_baseline:
|
||||
new_checkpoint = not (tip and tip == parent_tip)
|
||||
|
||||
row_dict["Experiment"] = exp.get("name", "")
|
||||
row_dict["rev"] = name_rev
|
||||
row_dict["typ"] = typ
|
||||
row_dict["Created"] = format_time(exp.get("timestamp"), fill_value, iso)
|
||||
row_dict["parent"] = parent
|
||||
row_dict["State"] = state
|
||||
row_dict["Executor"] = exp.get("executor", fill_value)
|
||||
|
||||
_extend_row(
|
||||
row_dict,
|
||||
metric_names,
|
||||
metric_headers,
|
||||
exp.get("metrics", {}).items(),
|
||||
precision,
|
||||
fill_value=fill_value,
|
||||
)
|
||||
_extend_row(
|
||||
row_dict,
|
||||
param_names,
|
||||
param_headers,
|
||||
exp.get("params", {}).items(),
|
||||
precision,
|
||||
fill_value=fill_value,
|
||||
)
|
||||
for dep in deps_names:
|
||||
hash_info = exp.get("deps", {}).get(dep, {}).get("hash")
|
||||
if hash_info is not None:
|
||||
hash_info = hash_info[:7]
|
||||
row_dict[dep] = hash_info
|
||||
yield list(row_dict.values())
|
||||
|
||||
|
||||
def _sort_column(sort_by, metric_names, param_names):
|
||||
path, _, sort_name = sort_by.rpartition(":")
|
||||
matches = set()
|
||||
|
||||
if path:
|
||||
if path in metric_names and sort_name in metric_names[path]:
|
||||
matches.add((path, sort_name, "metrics"))
|
||||
if path in param_names and sort_name in param_names[path]:
|
||||
matches.add((path, sort_name, "params"))
|
||||
else:
|
||||
for path in metric_names:
|
||||
if sort_name in metric_names[path]:
|
||||
matches.add((path, sort_name, "metrics"))
|
||||
for path in param_names:
|
||||
if sort_name in param_names[path]:
|
||||
matches.add((path, sort_name, "params"))
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches.pop()
|
||||
if len(matches) > 1:
|
||||
raise InvalidArgumentError(
|
||||
"Ambiguous sort column '{}' matched '{}'".format(
|
||||
sort_by,
|
||||
", ".join([f"{path}:{name}" for path, name, _ in matches]),
|
||||
)
|
||||
)
|
||||
raise InvalidArgumentError(f"Unknown sort column '{sort_by}'")
|
||||
|
||||
|
||||
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
|
||||
def _sort(item):
|
||||
rev, exp = item
|
||||
exp_data = exp.get("data", {})
|
||||
tip = exp_data.get("checkpoint_tip")
|
||||
if tip and tip != rev:
|
||||
# Sort checkpoint experiments by tip commit
|
||||
return _sort((tip, experiments[tip]))
|
||||
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
|
||||
val = flatten(data).get(sort_name)
|
||||
return val is None, val
|
||||
|
||||
ret = OrderedDict()
|
||||
if "baseline" in experiments:
|
||||
ret["baseline"] = experiments.pop("baseline")
|
||||
|
||||
ret.update(sorted(experiments.items(), key=_sort, reverse=reverse))
|
||||
return ret
|
||||
|
||||
|
||||
def format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
|
||||
if datetime_obj is None:
|
||||
return fill_value
|
||||
|
||||
if iso:
|
||||
return datetime_obj.isoformat()
|
||||
|
||||
if datetime_obj.date() == date.today():
|
||||
fmt = "%I:%M %p"
|
||||
else:
|
||||
fmt = "%b %d, %Y"
|
||||
return datetime_obj.strftime(fmt)
|
||||
|
||||
|
||||
def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
|
||||
from dvc.compare import _format_field, with_value
|
||||
|
||||
for fname, data in items:
|
||||
item = data.get("data", {})
|
||||
item = flatten(item) if isinstance(item, dict) else {fname: item}
|
||||
for name in names[fname]:
|
||||
value = with_value(
|
||||
item.get(name),
|
||||
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
|
||||
)
|
||||
# wrap field data in ui.rich_text, otherwise rich may
|
||||
# interpret unescaped braces from list/dict types as rich
|
||||
# markup tags
|
||||
value = ui.rich_text(str(_format_field(value, precision)))
|
||||
if name in headers:
|
||||
row[name] = value
|
||||
else:
|
||||
row[f"{fname}:{name}"] = value
|
||||
|
||||
|
||||
def experiments_table(
|
||||
all_experiments,
|
||||
headers,
|
||||
metric_headers,
|
||||
metric_names,
|
||||
param_headers,
|
||||
param_names,
|
||||
deps_names,
|
||||
sort_by=None,
|
||||
sort_order=None,
|
||||
precision=DEFAULT_PRECISION,
|
||||
fill_value=FILL_VALUE,
|
||||
iso=False,
|
||||
) -> "TabularData":
|
||||
from funcy import lconcat
|
||||
|
||||
from dvc.compare import TabularData
|
||||
|
||||
all_headers = lconcat(headers, metric_headers, param_headers, deps_names)
|
||||
td = TabularData(all_headers, fill_value=fill_value)
|
||||
for base_rev, experiments in all_experiments.items():
|
||||
rows = _collect_rows(
|
||||
base_rev,
|
||||
experiments,
|
||||
all_headers,
|
||||
metric_headers,
|
||||
param_headers,
|
||||
metric_names,
|
||||
param_names,
|
||||
deps_names,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
)
|
||||
td.extend(rows)
|
||||
|
||||
return td
|
||||
|
||||
|
||||
def prepare_exp_id(kwargs) -> "RichText":
|
||||
exp_name = kwargs["Experiment"]
|
||||
rev = kwargs["rev"]
|
||||
@@ -308,8 +59,9 @@ def baseline_styler(typ):
|
||||
return {"style": "bold"} if typ == "baseline" else {}
|
||||
|
||||
|
||||
def show_experiments( # noqa: C901
|
||||
all_experiments,
|
||||
def show_experiments( # noqa: C901, PLR0912
|
||||
td: "TabularData",
|
||||
headers: Dict[str, Iterable[str]],
|
||||
keep=None,
|
||||
drop=None,
|
||||
pager=True,
|
||||
@@ -318,40 +70,6 @@ def show_experiments( # noqa: C901
|
||||
pcp=False,
|
||||
**kwargs,
|
||||
):
|
||||
from funcy.seqs import flatten as flatten_list
|
||||
|
||||
metric_names, param_names, deps_names = _collect_names(all_experiments)
|
||||
|
||||
headers_list = [
|
||||
"Experiment",
|
||||
"rev",
|
||||
"typ",
|
||||
"Created",
|
||||
"parent",
|
||||
"State",
|
||||
"Executor",
|
||||
]
|
||||
|
||||
names = {**metric_names, **param_names}
|
||||
counter = Counter(flatten_list([list(a.keys()) for a in names.values()]))
|
||||
counter.update(headers_list)
|
||||
metric_headers = _normalize_headers(metric_names, counter)
|
||||
param_headers = _normalize_headers(param_names, counter)
|
||||
|
||||
td = experiments_table(
|
||||
all_experiments,
|
||||
headers_list,
|
||||
metric_headers,
|
||||
metric_names,
|
||||
param_headers,
|
||||
param_names,
|
||||
deps_names,
|
||||
kwargs.get("sort_by"),
|
||||
kwargs.get("sort_order"),
|
||||
kwargs.get("precision"),
|
||||
kwargs.get("fill_value"),
|
||||
kwargs.get("iso"),
|
||||
)
|
||||
if keep:
|
||||
for col in td.keys(): # noqa: SIM118
|
||||
if re.match(keep, col):
|
||||
@@ -368,11 +86,6 @@ def show_experiments( # noqa: C901
|
||||
td.column("Experiment")[:] = map(prepare_exp_id, td.as_dict(merge_headers))
|
||||
td.drop(*merge_headers[1:])
|
||||
|
||||
headers = {
|
||||
"metrics": metric_headers,
|
||||
"params": param_headers,
|
||||
"deps": deps_names,
|
||||
}
|
||||
styles = {
|
||||
"Experiment": {"no_wrap": True, "header_style": "black on grey93"},
|
||||
"Created": {"header_style": "black on grey93"},
|
||||
@@ -460,8 +173,10 @@ def _format_json(item):
|
||||
|
||||
class CmdExperimentsShow(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo.experiments.show import tabulate
|
||||
|
||||
try:
|
||||
all_experiments = self.repo.experiments.show(
|
||||
exps = self.repo.experiments.show(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
@@ -472,29 +187,36 @@ class CmdExperimentsShow(CmdBase):
|
||||
sha_only=self.args.sha,
|
||||
param_deps=self.args.param_deps,
|
||||
fetch_running=self.args.fetch_running,
|
||||
force=self.args.force,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to show experiments")
|
||||
return 1
|
||||
|
||||
if self.args.json:
|
||||
ui.write_json(all_experiments, default=_format_json)
|
||||
ui.write_json([exp.dumpd() for exp in exps], default=_format_json)
|
||||
else:
|
||||
precision = (
|
||||
self.args.precision or None if self.args.csv else DEFAULT_PRECISION
|
||||
)
|
||||
fill_value = "" if self.args.csv else FILL_VALUE
|
||||
iso = self.args.csv
|
||||
td, headers = tabulate(
|
||||
exps,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
sort_by=self.args.sort_by,
|
||||
sort_order=self.args.sort_order,
|
||||
)
|
||||
|
||||
show_experiments(
|
||||
all_experiments,
|
||||
td,
|
||||
headers,
|
||||
keep=self.args.keep,
|
||||
drop=self.args.drop,
|
||||
sort_by=self.args.sort_by,
|
||||
sort_order=self.args.sort_order,
|
||||
precision=precision,
|
||||
fill_value=fill_value,
|
||||
iso=iso,
|
||||
pager=not self.args.no_pager,
|
||||
csv=self.args.csv,
|
||||
markdown=self.args.markdown,
|
||||
@@ -650,4 +372,10 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
action="store_false",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Force re-collection of experiments instead of loading from exp cache.",
|
||||
)
|
||||
experiments_show_parser.set_defaults(func=CmdExperimentsShow)
|
||||
|
||||
+15
-2
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self): # noqa: C901
|
||||
def run(self): # noqa: C901, PLR0912
|
||||
from dvc.repo.gc import _validate_args
|
||||
|
||||
_validate_args(
|
||||
@@ -22,6 +22,8 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
cloud=self.args.cloud,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
|
||||
if self.args.rev:
|
||||
@@ -46,6 +48,10 @@ class CmdGC(CmdBase):
|
||||
|
||||
if self.args.all_experiments:
|
||||
msg += " and all experiments"
|
||||
|
||||
if self.args.not_in_remote:
|
||||
msg += " that are present in the DVC remote"
|
||||
|
||||
if self.args.repos:
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
@@ -74,6 +80,7 @@ class CmdGC(CmdBase):
|
||||
workspace=self.args.workspace,
|
||||
rev=self.args.rev,
|
||||
num=self.args.num,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -157,12 +164,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Keep data files for all experiments.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files that are not present in the remote.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-c",
|
||||
"--cloud",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Collect garbage in remote repository.",
|
||||
help="Collect garbage in remote storage in addition to local cache.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-r",
|
||||
|
||||
+11
-7
@@ -5,7 +5,6 @@ from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import CloneError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,12 +14,8 @@ class CmdGet(CmdBaseNoRepo):
|
||||
from dvc.api import get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
try:
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
ui.write(url, force=True)
|
||||
except DvcException:
|
||||
logger.exception("failed to show URL")
|
||||
return 1
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -32,6 +27,7 @@ class CmdGet(CmdBaseNoRepo):
|
||||
|
||||
def _get_file_from_repo(self):
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import CloneError
|
||||
|
||||
try:
|
||||
Repo.get(
|
||||
@@ -40,6 +36,7 @@ class CmdGet(CmdBaseNoRepo):
|
||||
out=self.args.out,
|
||||
rev=self.args.rev,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
return 0
|
||||
except CloneError:
|
||||
@@ -98,4 +95,11 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGet)
|
||||
|
||||
+13
-1
@@ -14,7 +14,12 @@ class CmdGetUrl(CmdBaseNoRepo):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
Repo.get_url(self.args.url, out=self.args.out, jobs=self.args.jobs)
|
||||
Repo.get_url(
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception("failed to get '%s'", self.args.url)
|
||||
@@ -46,4 +51,11 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGetUrl)
|
||||
|
||||
+2
-1
@@ -5,13 +5,14 @@ from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.scm import CloneError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdImport(CmdBase):
|
||||
def run(self):
|
||||
from dvc.scm import CloneError
|
||||
|
||||
try:
|
||||
self.repo.imp(
|
||||
self.args.url,
|
||||
|
||||
@@ -25,6 +25,7 @@ class CmdImportUrl(CmdBase):
|
||||
labels=self.args.labels,
|
||||
meta=self.args.meta,
|
||||
jobs=self.args.jobs,
|
||||
force=self.args.force,
|
||||
version_aware=self.args.version_aware,
|
||||
)
|
||||
except DvcException:
|
||||
@@ -112,6 +113,13 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
metavar="<number>",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"--version-aware",
|
||||
action="store_true",
|
||||
|
||||
@@ -302,7 +302,7 @@ class CmdMachineSsh(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
def add_parser(subparsers, parent_parser): # noqa: PLR0915
|
||||
from dvc.commands.config import parent_config_parser
|
||||
|
||||
machine_HELP = "Set up and manage cloud machines."
|
||||
|
||||
+56
-24
@@ -1,9 +1,9 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
|
||||
from funcy import first
|
||||
from funcy import compact, first, get_in
|
||||
|
||||
from dvc.cli import completion
|
||||
from dvc.cli.command import CmdBase
|
||||
@@ -12,14 +12,51 @@ from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.render.match import RendererWithErrors
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _show_json(renderers, split=False):
|
||||
def _show_json(
|
||||
renderers_with_errors: List["RendererWithErrors"],
|
||||
split=False,
|
||||
errors: Optional[Dict[str, Exception]] = None,
|
||||
):
|
||||
from dvc.render.convert import to_json
|
||||
from dvc.utils.serialize import encode_exception
|
||||
|
||||
result = {renderer.name: to_json(renderer, split) for renderer in renderers}
|
||||
ui.write_json(result)
|
||||
all_errors: List[Dict] = []
|
||||
data = {}
|
||||
|
||||
for renderer, src_errors, def_errors in renderers_with_errors:
|
||||
name = renderer.name
|
||||
data[name] = to_json(renderer, split)
|
||||
all_errors.extend(
|
||||
{
|
||||
"name": name,
|
||||
"rev": rev,
|
||||
"source": source,
|
||||
**encode_exception(e),
|
||||
}
|
||||
for rev, per_rev_src_errors in src_errors.items()
|
||||
for source, e in per_rev_src_errors.items()
|
||||
)
|
||||
all_errors.extend(
|
||||
{
|
||||
"name": name,
|
||||
"rev": rev,
|
||||
**encode_exception(e),
|
||||
}
|
||||
for rev, e in def_errors.items()
|
||||
)
|
||||
|
||||
# these errors are not tied to any renderers
|
||||
errors = errors or {}
|
||||
all_errors.extend({"rev": rev, **encode_exception(e)} for rev, e in errors.items())
|
||||
|
||||
ui.write_json(compact({"errors": all_errors, "data": data}), highlight=False)
|
||||
|
||||
|
||||
def _adjust_vega_renderers(renderers):
|
||||
@@ -76,10 +113,6 @@ class CmdPlots(CmdBase):
|
||||
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
|
||||
return {k: v for k, v in props.items() if v is not None}
|
||||
|
||||
def _config_files(self):
|
||||
if self.args.from_config:
|
||||
return {self.args.from_config}
|
||||
|
||||
def _html_template_path(self):
|
||||
html_template_path = self.args.html_template
|
||||
if not html_template_path:
|
||||
@@ -89,9 +122,9 @@ class CmdPlots(CmdBase):
|
||||
if html_template_path and not os.path.isabs(html_template_path):
|
||||
assert self.repo.dvc_dir
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path # noqa: RET504
|
||||
return html_template_path
|
||||
|
||||
def run(self) -> int: # noqa: C901
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_defs_renderers
|
||||
@@ -114,10 +147,9 @@ class CmdPlots(CmdBase):
|
||||
plots_data = self._func(
|
||||
targets=self.args.targets,
|
||||
props=self._props(),
|
||||
config_files=self._config_files(),
|
||||
)
|
||||
|
||||
if not plots_data:
|
||||
if not plots_data and not self.args.json:
|
||||
ui.error_write(
|
||||
"No plots were loaded, visualization file will not be created."
|
||||
)
|
||||
@@ -127,21 +159,27 @@ class CmdPlots(CmdBase):
|
||||
)
|
||||
|
||||
renderers_out = out if self.args.json else os.path.join(out, "static")
|
||||
|
||||
renderers = match_defs_renderers(
|
||||
renderers_with_errors = match_defs_renderers(
|
||||
data=plots_data,
|
||||
out=renderers_out,
|
||||
templates_dir=self.repo.plots.templates_dir,
|
||||
)
|
||||
if self.args.json:
|
||||
_show_json(renderers, self.args.split)
|
||||
errors = compact(
|
||||
{
|
||||
rev: get_in(data, ["definitions", "error"])
|
||||
for rev, data in plots_data.items()
|
||||
}
|
||||
)
|
||||
_show_json(renderers_with_errors, self.args.split, errors=errors)
|
||||
return 0
|
||||
|
||||
renderers = [r.renderer for r in renderers_with_errors]
|
||||
_adjust_vega_renderers(renderers)
|
||||
if self.args.show_vega:
|
||||
renderer = first(filter(lambda r: r.TYPE == "vega", renderers))
|
||||
if renderer:
|
||||
ui.write_json(json.loads(renderer.get_filled_template()))
|
||||
ui.write_json(renderer.get_filled_template(as_string=False))
|
||||
return 0
|
||||
|
||||
output_file: Path = (Path.cwd() / out).resolve() / "index.html"
|
||||
@@ -150,7 +188,7 @@ class CmdPlots(CmdBase):
|
||||
render_html(
|
||||
renderers=renderers,
|
||||
output_file=output_file,
|
||||
template_path=self._html_template_path(),
|
||||
html_template=self._html_template_path(),
|
||||
)
|
||||
|
||||
ui.write(output_file.as_uri())
|
||||
@@ -412,9 +450,3 @@ def _add_ui_arguments(parser):
|
||||
help="Custom HTML template for VEGA visualization.",
|
||||
metavar="<path>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--from-config",
|
||||
default=None,
|
||||
metavar="<path>",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.experiments.show import format_time
|
||||
from dvc.compare import TabularData
|
||||
from dvc.ui import ui
|
||||
|
||||
@@ -15,9 +13,9 @@ class CmdQueueStatus(CmdBase):
|
||||
"""Show queue task and worker status."""
|
||||
|
||||
def run(self) -> int:
|
||||
result: List[
|
||||
Dict[str, Optional[str]]
|
||||
] = self.repo.experiments.celery_queue.status()
|
||||
from dvc.repo.experiments.show import format_time
|
||||
|
||||
result = self.repo.experiments.celery_queue.status()
|
||||
if result:
|
||||
all_headers = ["Task", "Name", "Created", "Status"]
|
||||
td = TabularData(all_headers)
|
||||
|
||||
@@ -38,6 +38,7 @@ class CmdRepro(CmdBase):
|
||||
"recursive": self.args.recursive,
|
||||
"force_downstream": self.args.force_downstream,
|
||||
"pull": self.args.pull,
|
||||
"allow_missing": self.args.allow_missing,
|
||||
}
|
||||
|
||||
@property
|
||||
@@ -135,6 +136,12 @@ and then the stage name name.
|
||||
"from the run-cache."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--allow-missing",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=("Skip stages with missing data but no other changes."),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--dry",
|
||||
action="store_true",
|
||||
|
||||
@@ -31,8 +31,7 @@ class CmdUpdate(CmdBase):
|
||||
def add_parser(subparsers, parent_parser):
|
||||
UPDATE_HELP = (
|
||||
"Update data artifact imported (via dvc import or dvc import-url) "
|
||||
"from an external DVC repository or URL, or update data from a "
|
||||
"DVC worktree remote."
|
||||
"from an external DVC repository or URL."
|
||||
)
|
||||
update_parser = subparsers.add_parser(
|
||||
"update",
|
||||
|
||||
+3
-3
@@ -35,7 +35,7 @@ def with_value(value, default):
|
||||
|
||||
|
||||
class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
def __init__(self, columns: Sequence[str], fill_value: str = ""):
|
||||
def __init__(self, columns: Sequence[str], fill_value: Optional[str] = ""):
|
||||
self._columns: Dict[str, Column] = {name: Column() for name in columns}
|
||||
self._keys: List[str] = list(columns)
|
||||
self._fill_value = fill_value
|
||||
@@ -209,7 +209,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
keys = self.keys() if cols is None else set(cols)
|
||||
return [{k: self._columns[k][i] for k in keys} for i in range(len(self))]
|
||||
|
||||
def dropna( # noqa: C901
|
||||
def dropna( # noqa: C901, PLR0912
|
||||
self,
|
||||
axis: str = "rows",
|
||||
how="any",
|
||||
@@ -371,7 +371,7 @@ def diff_table(
|
||||
return td
|
||||
|
||||
|
||||
def show_diff(
|
||||
def show_diff( # noqa: PLR0913
|
||||
diff,
|
||||
title: str,
|
||||
old: bool = True,
|
||||
|
||||
+15
-18
@@ -79,9 +79,6 @@ class Config(dict):
|
||||
ConfigError: thrown if config has an invalid format.
|
||||
"""
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
SYSTEM_LEVELS = ("system", "global")
|
||||
REPO_LEVELS = ("repo", "local")
|
||||
# In the order they shadow each other
|
||||
@@ -103,28 +100,32 @@ class Config(dict):
|
||||
self.wfs = LocalFileSystem()
|
||||
self.fs = fs or self.wfs
|
||||
|
||||
if not dvc_dir:
|
||||
try:
|
||||
from dvc.repo import Repo
|
||||
|
||||
self.dvc_dir = Repo.find_dvc_dir()
|
||||
except NotDvcRepoError:
|
||||
self.dvc_dir = None
|
||||
else:
|
||||
if dvc_dir:
|
||||
self.dvc_dir = self.fs.path.abspath(self.fs.path.realpath(dvc_dir))
|
||||
|
||||
self.load(validate=validate, config=config)
|
||||
|
||||
@classmethod
|
||||
def from_cwd(cls, fs: Optional["FileSystem"] = None, **kwargs):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
dvc_dir = Repo.find_dvc_dir(fs=fs)
|
||||
except NotDvcRepoError:
|
||||
dvc_dir = None
|
||||
|
||||
return cls(dvc_dir=dvc_dir, fs=fs, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from appdirs import site_config_dir, user_config_dir
|
||||
from dvc.dirs import global_config_dir, system_config_dir
|
||||
|
||||
assert level in ("global", "system")
|
||||
|
||||
if level == "global":
|
||||
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return global_config_dir()
|
||||
if level == "system":
|
||||
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
return system_config_dir()
|
||||
|
||||
@cached_property
|
||||
def files(self) -> Dict[str, str]:
|
||||
@@ -170,10 +171,6 @@ class Config(dict):
|
||||
self.clear()
|
||||
self.update(conf)
|
||||
|
||||
# Add resolved default cache.dir
|
||||
if not self["cache"].get("dir") and self.dvc_dir:
|
||||
self["cache"]["dir"] = os.path.join(self.dvc_dir, "cache")
|
||||
|
||||
def _get_fs(self, level):
|
||||
# NOTE: this might be a Gitfs, which doesn't see things outside of
|
||||
# the repo.
|
||||
|
||||
+57
-12
@@ -1,8 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import walk_values
|
||||
from voluptuous import All, Any, Coerce, Invalid, Lower, Optional, Range, Schema
|
||||
from funcy import once, walk_values
|
||||
from voluptuous import (
|
||||
REMOVE_EXTRA,
|
||||
All,
|
||||
Any,
|
||||
Coerce,
|
||||
Invalid,
|
||||
Lower,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Bool = All(
|
||||
Lower,
|
||||
@@ -30,7 +43,7 @@ def supported_cache_type(types):
|
||||
return types
|
||||
|
||||
|
||||
def Choices(*choices):
|
||||
def Choices(*choices): # noqa: N802
|
||||
"""Checks that value belongs to the specified set of values
|
||||
|
||||
Args:
|
||||
@@ -40,7 +53,7 @@ def Choices(*choices):
|
||||
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
|
||||
|
||||
|
||||
def ByUrl(mapping):
|
||||
def ByUrl(mapping): # noqa: N802
|
||||
schemas = walk_values(Schema, mapping)
|
||||
|
||||
def validate(data):
|
||||
@@ -49,7 +62,9 @@ def ByUrl(mapping):
|
||||
|
||||
parsed = urlparse(data["url"])
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
|
||||
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}://")
|
||||
@@ -63,6 +78,27 @@ class RelPath(str):
|
||||
pass
|
||||
|
||||
|
||||
class FeatureSchema(Schema):
|
||||
def __init__(self, schema, required=False):
|
||||
super().__init__(schema, required=required, extra=REMOVE_EXTRA)
|
||||
|
||||
@staticmethod
|
||||
@once
|
||||
def _log_deprecated(keys):
|
||||
# only run this once per session
|
||||
message = "%s config option%s unsupported"
|
||||
paths = ", ".join(f"'feature.{key}'" for key in keys)
|
||||
pluralize = " is" if len(keys) == 1 else "s are"
|
||||
logger.warning(message, paths, pluralize)
|
||||
|
||||
def __call__(self, data):
|
||||
ret = super().__call__(data)
|
||||
extra_keys = data.keys() - ret.keys()
|
||||
if extra_keys:
|
||||
self._log_deprecated(sorted(extra_keys))
|
||||
return ret
|
||||
|
||||
|
||||
REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
@@ -114,6 +150,7 @@ SCHEMA = {
|
||||
Optional("autostage", default=False): Bool,
|
||||
Optional("experiments"): Bool, # obsoleted
|
||||
Optional("check_update", default=True): Bool,
|
||||
"site_cache_dir": str,
|
||||
"machine": Lower,
|
||||
},
|
||||
"cache": {
|
||||
@@ -178,6 +215,7 @@ SCHEMA = {
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
@@ -237,12 +275,12 @@ SCHEMA = {
|
||||
)
|
||||
},
|
||||
"state": {
|
||||
"dir": str,
|
||||
"dir": str, # obsoleted
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str,
|
||||
"dir": str, # obsoleted
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
@@ -260,11 +298,12 @@ SCHEMA = {
|
||||
},
|
||||
},
|
||||
# section for experimental features
|
||||
"feature": {
|
||||
Optional("machine", default=False): Bool,
|
||||
# enabled by default. It's of no use, kept for backward compatibility.
|
||||
Optional("parametrization", default=True): Bool,
|
||||
},
|
||||
# only specified keys are validated, others get logged and then ignored/removed
|
||||
"feature": FeatureSchema(
|
||||
{
|
||||
Optional("machine", default=False): Bool,
|
||||
},
|
||||
),
|
||||
"plots": {
|
||||
"html_template": str,
|
||||
Optional("auto_open", default=False): Bool,
|
||||
@@ -288,4 +327,10 @@ SCHEMA = {
|
||||
"config_dir": str,
|
||||
"config_name": str,
|
||||
},
|
||||
"studio": {
|
||||
"token": str,
|
||||
"url": str,
|
||||
"repo_url": str,
|
||||
Optional("offline", default=False): Bool,
|
||||
},
|
||||
}
|
||||
|
||||
+23
-5
@@ -5,7 +5,7 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from subprocess import Popen
|
||||
from subprocess import Popen # nosec B404
|
||||
from typing import List
|
||||
|
||||
from dvc.env import DVC_DAEMON
|
||||
@@ -14,19 +14,26 @@ from dvc.utils import fix_env, is_binary
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs):
|
||||
def _suppress_resource_warning(popen: Popen):
|
||||
"""Sets the returncode to avoid ResourceWarning when popen is garbage collected."""
|
||||
# only use for daemon processes.
|
||||
# See https://bugs.python.org/issue38890.
|
||||
popen.returncode = 0
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs) -> Popen:
|
||||
prefix = [sys.executable]
|
||||
if not is_binary():
|
||||
main_entrypoint = os.path.join(
|
||||
os.path.abspath(os.path.dirname(__file__)), "__main__.py"
|
||||
)
|
||||
prefix += [main_entrypoint]
|
||||
return Popen(prefix + cmd, close_fds=True, shell=False, **kwargs)
|
||||
return Popen(prefix + cmd, close_fds=True, shell=False, **kwargs) # nosec B603
|
||||
|
||||
|
||||
def _spawn_windows(cmd, env):
|
||||
if sys.platform == "win32":
|
||||
from subprocess import (
|
||||
from subprocess import ( # nosec B404
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
@@ -40,12 +47,22 @@ def _spawn_windows(cmd, env):
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
|
||||
_popen(cmd, env=env, creationflags=creationflags, startupinfo=startupinfo)
|
||||
popen = _popen(
|
||||
cmd, env=env, creationflags=creationflags, startupinfo=startupinfo
|
||||
)
|
||||
_suppress_resource_warning(popen)
|
||||
|
||||
|
||||
def _spawn_posix(cmd, env):
|
||||
from dvc.cli import main
|
||||
|
||||
# `fork` will copy buffers, so we need to flush them before forking.
|
||||
# Otherwise, we will get duplicated outputs.
|
||||
if sys.stdout and not sys.stdout.closed:
|
||||
sys.stdout.flush()
|
||||
if sys.stderr and not sys.stderr.closed:
|
||||
sys.stderr.flush()
|
||||
|
||||
# NOTE: using os._exit instead of sys.exit, because dvc built
|
||||
# with PyInstaller has trouble with SystemExit exception and throws
|
||||
# errors such as "[26338] Failed to execute script __main__"
|
||||
@@ -72,6 +89,7 @@ def _spawn_posix(cmd, env):
|
||||
sys.stdin.close()
|
||||
sys.stdout.close()
|
||||
sys.stderr.close()
|
||||
os.closerange(0, 3)
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
# workaround for MacOS bug
|
||||
|
||||
+3
-3
@@ -80,7 +80,7 @@ class AsciiCanvas:
|
||||
|
||||
self.canvas[y][x] = char
|
||||
|
||||
def line(self, x0, y0, x1, y1, char): # noqa: C901
|
||||
def line(self, x0, y0, x1, y1, char): # noqa: C901, PLR0912
|
||||
"""Create a line on ASCII canvas.
|
||||
|
||||
Args:
|
||||
@@ -213,8 +213,8 @@ def draw(vertices, edges):
|
||||
# pylint: disable=too-many-locals
|
||||
# NOTE: coordinates might me negative, so we need to shift
|
||||
# everything to the positive plane before we actually draw it.
|
||||
Xs = [] # pylint: disable=invalid-name
|
||||
Ys = [] # pylint: disable=invalid-name
|
||||
Xs = [] # noqa: N806, pylint: disable=invalid-name
|
||||
Ys = [] # noqa: N806, pylint: disable=invalid-name
|
||||
|
||||
sug = _build_sugiyama_layout(vertices, edges)
|
||||
|
||||
|
||||
+18
-13
@@ -18,10 +18,11 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remote:
|
||||
def __init__(self, name: str, path: str, fs: "FileSystem", **config):
|
||||
def __init__(self, name: str, path: str, fs: "FileSystem", *, index=None, **config):
|
||||
self.path = path
|
||||
self.fs = fs
|
||||
self.name = name
|
||||
self.index = index
|
||||
|
||||
self.worktree: bool = config.pop("worktree", False)
|
||||
self.config = config
|
||||
@@ -34,7 +35,7 @@ class Remote:
|
||||
if self.worktree:
|
||||
path = self.fs.path.join(path, ".dvc", "cache")
|
||||
|
||||
return get_odb(self.fs, path, **self.config)
|
||||
return get_odb(self.fs, path, hash_name="md5", **self.config)
|
||||
|
||||
|
||||
class DataCloud:
|
||||
@@ -74,19 +75,23 @@ class DataCloud:
|
||||
config["version_aware"] = True
|
||||
|
||||
fs = cls(**config)
|
||||
config["tmp_dir"] = self.repo.index_db_dir
|
||||
return Remote(name, fs_path, fs, **config)
|
||||
config["tmp_dir"] = self.repo.site_cache_dir
|
||||
if self.repo.data_index is not None:
|
||||
index = self.repo.data_index.view(("remote", name))
|
||||
else:
|
||||
index = None
|
||||
return Remote(name, fs_path, fs, index=index, **config)
|
||||
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
"no remote specified. Setup default remote with\n"
|
||||
f"no remote specified in {self.repo}. Setup default remote with\n"
|
||||
" dvc remote default <remote name>\n"
|
||||
"or use:\n"
|
||||
" dvc {} -r <remote name>".format(command)
|
||||
f" dvc {command} -r <remote name>"
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
"no remote specified. Create a default remote with\n"
|
||||
f"no remote specified in {self.repo}. Create a default remote with\n"
|
||||
" dvc remote add -d <remote name> <remote url>"
|
||||
)
|
||||
|
||||
@@ -147,12 +152,12 @@ class DataCloud:
|
||||
"""
|
||||
odb = odb or self.get_remote_odb(remote, "push")
|
||||
return self.transfer(
|
||||
self.repo.odb.local,
|
||||
self.repo.cache.local,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.odb.local,
|
||||
cache_odb=self.repo.cache.local,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
|
||||
@@ -175,11 +180,11 @@ class DataCloud:
|
||||
odb = odb or self.get_remote_odb(remote, "pull")
|
||||
return self.transfer(
|
||||
odb,
|
||||
self.repo.odb.local,
|
||||
self.repo.cache.local,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
src_index=get_index(odb),
|
||||
cache_odb=self.repo.odb.local,
|
||||
cache_odb=self.repo.cache.local,
|
||||
verify=odb.verify,
|
||||
validate_status=self._log_missing,
|
||||
)
|
||||
@@ -206,12 +211,12 @@ class DataCloud:
|
||||
if not odb:
|
||||
odb = self.get_remote_odb(remote, "status")
|
||||
return compare_status(
|
||||
self.repo.odb.local,
|
||||
self.repo.cache.local,
|
||||
odb,
|
||||
objs,
|
||||
jobs=jobs,
|
||||
dest_index=get_index(odb),
|
||||
cache_odb=self.repo.odb.local,
|
||||
cache_odb=self.repo.cache.local,
|
||||
)
|
||||
|
||||
def get_url_for(self, remote, checksum):
|
||||
|
||||
@@ -2,9 +2,9 @@ import logging
|
||||
import os
|
||||
import typing
|
||||
from collections import defaultdict
|
||||
from typing import Dict, cast
|
||||
from typing import Dict
|
||||
|
||||
import dpath.util
|
||||
import dpath
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
@@ -87,17 +87,17 @@ class ParamsDependency(Dependency):
|
||||
if flatten:
|
||||
for param in self.params:
|
||||
try:
|
||||
ret[param] = dpath.util.get(config, param, separator=".")
|
||||
ret[param] = dpath.get(config, param, separator=".")
|
||||
except KeyError:
|
||||
continue
|
||||
return ret
|
||||
|
||||
from dpath.util import merge
|
||||
from dpath import merge
|
||||
|
||||
for param in self.params:
|
||||
merge(
|
||||
ret,
|
||||
dpath.util.search(config, param, separator="."),
|
||||
dpath.search(config, param, separator="."),
|
||||
separator=".",
|
||||
)
|
||||
return ret
|
||||
@@ -111,7 +111,8 @@ class ParamsDependency(Dependency):
|
||||
from funcy import ldistinct
|
||||
|
||||
status: Dict[str, Any] = defaultdict(dict)
|
||||
info = cast(dict, self.hash_info.value) if self.hash_info else {}
|
||||
assert isinstance(self.hash_info.value, dict)
|
||||
info = self.hash_info.value if self.hash_info else {}
|
||||
actual = self.read_params()
|
||||
|
||||
# NOTE: we want to preserve the order of params as specified in the
|
||||
|
||||
+24
-152
@@ -1,24 +1,14 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Tuple, Union
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union
|
||||
|
||||
from voluptuous import Required
|
||||
|
||||
from dvc.prompt import confirm
|
||||
from dvc.utils import as_posix
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import ContextManager
|
||||
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.fs import DVCFileSystem
|
||||
from dvc.stage import Stage
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
|
||||
class RepoDependency(Dependency):
|
||||
@@ -37,10 +27,11 @@ class RepoDependency(Dependency):
|
||||
|
||||
def __init__(self, def_repo: Dict[str, str], stage: "Stage", *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
self._objs: Dict[str, "HashFile"] = {}
|
||||
self._meta: Dict[str, "Meta"] = {}
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
self.fs = self._make_fs()
|
||||
self.fs_path = as_posix(self.def_path)
|
||||
|
||||
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
||||
return None
|
||||
|
||||
@@ -52,8 +43,8 @@ class RepoDependency(Dependency):
|
||||
return f"{self.def_path} ({self.def_repo[self.PARAM_URL]})"
|
||||
|
||||
def workspace_status(self):
|
||||
current = self.get_obj(locked=True).hash_info
|
||||
updated = self.get_obj(locked=False).hash_info
|
||||
current = self._make_fs(locked=True).repo.get_rev()
|
||||
updated = self._make_fs(locked=False).repo.get_rev()
|
||||
|
||||
if current != updated:
|
||||
return {str(self): "update available"}
|
||||
@@ -64,33 +55,18 @@ class RepoDependency(Dependency):
|
||||
return self.workspace_status()
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
rev = self.fs.repo.get_rev()
|
||||
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
|
||||
def dumpd(self, **kwargs) -> Dict[str, Union[str, Dict[str, str]]]:
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def download(self, to: "Output", jobs: Optional[int] = None):
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
|
||||
for odb, objs in self.get_used_objs().items():
|
||||
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
|
||||
|
||||
obj = self.get_obj()
|
||||
checkout(
|
||||
to.fs_path,
|
||||
to.fs,
|
||||
obj,
|
||||
self.repo.odb.local,
|
||||
ignore=None,
|
||||
state=self.repo.state,
|
||||
prompt=confirm,
|
||||
)
|
||||
|
||||
def update(self, rev: Optional[str] = None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
with self._make_repo(locked=False) as repo:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
|
||||
self.fs = self._make_fs(rev=rev, locked=False)
|
||||
self.def_repo[self.PARAM_REV_LOCK] = self.fs.repo.get_rev()
|
||||
|
||||
def changed_checksum(self) -> bool:
|
||||
# From current repo point of view what describes RepoDependency is its
|
||||
@@ -98,124 +74,20 @@ class RepoDependency(Dependency):
|
||||
# immutable, hence its impossible for checksum to change.
|
||||
return False
|
||||
|
||||
def get_used_objs(self, **kwargs) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
used, _, _ = self._get_used_and_obj(**kwargs)
|
||||
return used
|
||||
def _make_fs(
|
||||
self, rev: Optional[str] = None, locked: bool = True
|
||||
) -> "DVCFileSystem":
|
||||
from dvc.fs import DVCFileSystem
|
||||
|
||||
def _get_used_and_obj(
|
||||
self, obj_only: bool = False, **kwargs
|
||||
) -> Tuple[Dict[Optional["ObjectDB"], Set["HashInfo"]], "Meta", "HashFile"]:
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.exceptions import NoOutputOrStageError, PathMissingError
|
||||
from dvc.utils import as_posix
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.tree import Tree, TreeError
|
||||
config = {"cache": self.repo.config["cache"]}
|
||||
config["cache"]["dir"] = self.repo.cache.local.path
|
||||
|
||||
local_odb = self.repo.odb.local
|
||||
locked = kwargs.pop("locked", True)
|
||||
with self._make_repo(locked=locked, cache_dir=local_odb.path) as repo:
|
||||
used_obj_ids = defaultdict(set)
|
||||
rev = repo.get_rev()
|
||||
if locked and self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = rev
|
||||
|
||||
if not obj_only:
|
||||
try:
|
||||
for odb, obj_ids in repo.used_objs(
|
||||
[os.path.join(repo.root_dir, self.def_path)],
|
||||
force=True,
|
||||
jobs=kwargs.get("jobs"),
|
||||
recursive=True,
|
||||
).items():
|
||||
if odb is None:
|
||||
odb = repo.cloud.get_remote_odb()
|
||||
odb.read_only = True
|
||||
self._check_circular_import(odb, obj_ids)
|
||||
used_obj_ids[odb].update(obj_ids)
|
||||
except (NoRemoteError, NoOutputOrStageError):
|
||||
pass # noqa: S110
|
||||
|
||||
try:
|
||||
object_store, meta, obj = build(
|
||||
local_odb,
|
||||
as_posix(self.def_path),
|
||||
repo.dvcfs,
|
||||
local_odb.fs.PARAM_CHECKSUM,
|
||||
)
|
||||
except (FileNotFoundError, TreeError) as exc:
|
||||
raise PathMissingError(
|
||||
self.def_path, self.def_repo[self.PARAM_URL]
|
||||
) from exc
|
||||
object_store = copy(object_store)
|
||||
object_store.read_only = True
|
||||
|
||||
self._objs[rev] = obj
|
||||
self._meta[rev] = meta
|
||||
|
||||
used_obj_ids[object_store].add(obj.hash_info)
|
||||
if isinstance(obj, Tree):
|
||||
used_obj_ids[object_store].update(oid for _, _, oid in obj)
|
||||
return used_obj_ids, meta, obj
|
||||
|
||||
def _check_circular_import(self, odb: "ObjectDB", obj_ids: Set["HashInfo"]) -> None:
|
||||
from dvc.exceptions import CircularImportError
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc_data.hashfile.db.reference import ReferenceHashFileDB
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
|
||||
if not isinstance(odb, ReferenceHashFileDB):
|
||||
return
|
||||
|
||||
def iter_objs():
|
||||
for hash_info in obj_ids:
|
||||
if hash_info.isdir:
|
||||
tree = Tree.load(odb, hash_info)
|
||||
yield from (odb.get(hi.value) for _, _, hi in tree)
|
||||
else:
|
||||
assert hash_info.value
|
||||
yield odb.get(hash_info.value)
|
||||
|
||||
checked_urls = set()
|
||||
for obj in iter_objs():
|
||||
if not isinstance(obj.fs, DVCFileSystem):
|
||||
continue
|
||||
if obj.fs.repo_url in checked_urls or obj.fs.repo.root_dir in checked_urls:
|
||||
continue
|
||||
self_url = self.repo.url or self.repo.root_dir
|
||||
if (
|
||||
obj.fs.repo_url is not None
|
||||
and obj.fs.repo_url == self_url
|
||||
or obj.fs.repo.root_dir == self.repo.root_dir
|
||||
):
|
||||
raise CircularImportError(self, obj.fs.repo_url, self_url)
|
||||
checked_urls.update([obj.fs.repo_url, obj.fs.repo.root_dir])
|
||||
|
||||
def get_obj(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._objs:
|
||||
return self._objs[rev]
|
||||
_, _, obj = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
return DVCFileSystem(
|
||||
url=self.def_repo[self.PARAM_URL],
|
||||
rev=rev or self._get_rev(locked=locked),
|
||||
subrepos=True,
|
||||
config=config,
|
||||
)
|
||||
return obj
|
||||
|
||||
def get_meta(self, filter_info=None, **kwargs):
|
||||
locked = kwargs.get("locked", True)
|
||||
rev = self._get_rev(locked=locked)
|
||||
if rev in self._meta:
|
||||
return self._meta[rev]
|
||||
_, meta, _ = self._get_used_and_obj(
|
||||
obj_only=True, filter_info=filter_info, **kwargs
|
||||
)
|
||||
return meta
|
||||
|
||||
def _make_repo(self, locked: bool = True, **kwargs) -> "ContextManager[Repo]":
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
d = self.def_repo
|
||||
rev = self._get_rev(locked=locked)
|
||||
return external_repo(d[self.PARAM_URL], rev=rev, **kwargs)
|
||||
|
||||
def _get_rev(self, locked: bool = True):
|
||||
d = self.def_repo
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
import platformdirs
|
||||
|
||||
from . import env
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
|
||||
def system_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.site_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def global_config_dir():
|
||||
return os.getenv(env.DVC_SYSTEM_CONFIG_DIR) or platformdirs.user_config_dir(
|
||||
APPNAME, APPAUTHOR
|
||||
)
|
||||
|
||||
|
||||
def site_cache_dir():
|
||||
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
|
||||
APPNAME, APPAUTHOR, opinion=True
|
||||
)
|
||||
+12
-8
@@ -187,11 +187,12 @@ class FileMixin:
|
||||
|
||||
class SingleStageFile(FileMixin):
|
||||
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER
|
||||
from dvc.stage.loader import SingleStageLoader as LOADER # noqa: N814
|
||||
|
||||
metrics: List[str] = []
|
||||
plots: Any = {}
|
||||
params: List[str] = []
|
||||
artifacts: Dict[str, Optional[Dict[str, Any]]] = {}
|
||||
|
||||
@property
|
||||
def stage(self) -> "Stage":
|
||||
@@ -231,7 +232,7 @@ class ProjectFile(FileMixin):
|
||||
"""Abstraction for pipelines file, .yaml + .lock combined."""
|
||||
|
||||
from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
|
||||
from dvc.stage.loader import StageLoader as LOADER
|
||||
from dvc.stage.loader import StageLoader as LOADER # noqa: N814
|
||||
|
||||
@property
|
||||
def _lockfile(self):
|
||||
@@ -323,6 +324,10 @@ class ProjectFile(FileMixin):
|
||||
def params(self) -> List[str]:
|
||||
return self.contents.get("params", [])
|
||||
|
||||
@property
|
||||
def artifacts(self) -> Dict[str, Optional[Dict[str, Any]]]:
|
||||
return self.contents.get("artifacts", {})
|
||||
|
||||
def remove(self, force=False):
|
||||
if not force:
|
||||
logger.warning("Cannot remove pipeline file.")
|
||||
@@ -366,7 +371,7 @@ def get_lockfile_schema(d):
|
||||
|
||||
|
||||
def migrate_lock_v1_to_v2(d, version_info):
|
||||
stages = {k: v for k, v in d.items()}
|
||||
stages = dict(d)
|
||||
|
||||
for key in stages:
|
||||
d.pop(key)
|
||||
@@ -409,11 +414,10 @@ class Lockfile(FileMixin):
|
||||
if version == LOCKFILE_VERSION.V1:
|
||||
logger.info("Migrating lock file '%s' from v1 to v2", self.relpath)
|
||||
migrate_lock_v1_to_v2(data, self.latest_version_info)
|
||||
else:
|
||||
if not data:
|
||||
data.update(self.latest_version_info)
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
elif not data:
|
||||
data.update(self.latest_version_info)
|
||||
# order is important, meta should always be at the top
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
|
||||
+15
-7
@@ -1,11 +1,19 @@
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_CHECKPOINT = "DVC_CHECKPOINT"
|
||||
DVC_DAEMON = "DVC_DAEMON"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_GLOBAL_CONFIG_DIR = "DVC_GLOBAL_CONFIG_DIR"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_PAGER = "DVC_PAGER"
|
||||
DVC_ROOT = "DVC_ROOT"
|
||||
DVCLIVE_RESUME = "DVCLIVE_RESUME"
|
||||
DVC_IGNORE_ISATTY = "DVC_IGNORE_ISATTY"
|
||||
DVC_EXP_BASELINE_REV = "DVC_EXP_BASELINE_REV"
|
||||
DVC_EXP_NAME = "DVC_EXP_NAME"
|
||||
DVC_EXP_GIT_REMOTE = "DVC_EXP_GIT_REMOTE"
|
||||
DVC_EXP_AUTO_PUSH = "DVC_EXP_AUTO_PUSH"
|
||||
DVC_NO_ANALYTICS = "DVC_NO_ANALYTICS"
|
||||
DVC_SHOW_TRACEBACK = "DVC_SHOW_TRACEBACK"
|
||||
DVC_SITE_CACHE_DIR = "DVC_SITE_CACHE_DIR"
|
||||
DVC_STUDIO_OFFLINE = "DVC_STUDIO_OFFLINE"
|
||||
DVC_STUDIO_REPO_URL = "DVC_STUDIO_REPO_URL"
|
||||
DVC_STUDIO_TOKEN = "DVC_STUDIO_TOKEN" # noqa: S105 # nosec B105
|
||||
DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||
|
||||
+22
-12
@@ -1,4 +1,5 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
import errno
|
||||
from typing import Dict, List
|
||||
|
||||
from dvc.utils import format_link
|
||||
@@ -31,14 +32,19 @@ class OutputDuplicationError(DvcException):
|
||||
|
||||
assert isinstance(output, str)
|
||||
assert all(hasattr(stage, "relpath") for stage in stages)
|
||||
msg = ""
|
||||
stage_names = [s.addressing for s in stages]
|
||||
stages_str = " ".join(stage_names)
|
||||
if len(stages) == 1:
|
||||
msg = "output '{}' is already specified in {}.".format(
|
||||
output, first(stages)
|
||||
)
|
||||
stage_name = first(stages)
|
||||
msg = f"output '{output}' is already specified in {stage_name}."
|
||||
else:
|
||||
msg = "output '{}' is already specified in stages:\n{}".format(
|
||||
output, "\n".join(f"\t- {s.addressing}" for s in stages)
|
||||
output, "\n".join(f"\t- {s}" for s in stage_names)
|
||||
)
|
||||
msg += (
|
||||
f"\nUse `dvc remove {stages_str}` to stop tracking the overlapping output."
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.stages = stages
|
||||
self.output = output
|
||||
@@ -201,6 +207,18 @@ class ETagMismatchError(DvcException):
|
||||
)
|
||||
|
||||
|
||||
class FileExistsLocallyError(FileExistsError, DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
import os.path
|
||||
|
||||
self.path = path
|
||||
hint = "" if hint is None else f". {hint}"
|
||||
path_typ = "directory" if os.path.isdir(path) else "file"
|
||||
msg = f"The {path_typ} '{path}' already exists locally{hint}"
|
||||
super().__init__(msg)
|
||||
self.errno = errno.EEXIST
|
||||
|
||||
|
||||
class FileMissingError(DvcException):
|
||||
def __init__(self, path, hint=None):
|
||||
self.path = path
|
||||
@@ -333,14 +351,6 @@ class CacheLinkError(DvcException):
|
||||
self.fs_paths = fs_paths
|
||||
|
||||
|
||||
class CircularImportError(DvcException):
|
||||
def __init__(self, dep, a, b):
|
||||
super().__init__(
|
||||
f"'{dep}' contains invalid circular import. "
|
||||
f"DVC repo '{a}' already imports from '{b}'."
|
||||
)
|
||||
|
||||
|
||||
class PrettyDvcException(DvcException):
|
||||
def __pretty_exc__(self, **kwargs):
|
||||
"""Print prettier exception message."""
|
||||
|
||||
+2
-2
@@ -6,7 +6,6 @@ from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import utils # noqa: F401
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
LocalFileSystem,
|
||||
MemoryFileSystem,
|
||||
@@ -17,6 +16,7 @@ from dvc_objects.fs import ( # noqa: F401
|
||||
localfs,
|
||||
registry,
|
||||
system,
|
||||
utils,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401
|
||||
from dvc_objects.fs.errors import ( # noqa: F401
|
||||
@@ -121,7 +121,7 @@ def _get_cloud_fs(repo_config, **kwargs):
|
||||
|
||||
remote_conf = get_fs_config(repo_config, **kwargs)
|
||||
try:
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf)
|
||||
remote_conf = SCHEMA["remote"][str](remote_conf) # type: ignore[index]
|
||||
except Invalid as exc:
|
||||
raise RepoConfigError(str(exc)) from None
|
||||
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# pylint: disable=unused-import
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, BinaryIO, Dict, Optional, Union
|
||||
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_objects.fs.callbacks import ( # noqa: F401
|
||||
@@ -54,7 +54,8 @@ class RichCallback(Callback):
|
||||
disable=self.disable,
|
||||
console=ui.error_console,
|
||||
)
|
||||
return cast(RichTransferProgress, self._stack.enter_context(progress))
|
||||
self._stack.enter_context(progress)
|
||||
return progress
|
||||
|
||||
@cached_property
|
||||
def task(self) -> "TaskID":
|
||||
|
||||
+34
-6
@@ -251,12 +251,15 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
try:
|
||||
return self.repo.fs.open(fs_path, mode=mode)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.open(dvc_path, mode=mode)
|
||||
kw = {}
|
||||
if kwargs.get("cache_remote_stream", False):
|
||||
kw["cache_odb"] = repo.cache.local
|
||||
return dvc_fs.open(dvc_path, mode=mode, **kw)
|
||||
|
||||
def isdvc(self, path, **kwargs) -> bool:
|
||||
"""Is this entry dvc-tracked?"""
|
||||
@@ -265,19 +268,22 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs is not None and dvc_fs.isdvc(dvc_path, **kwargs)
|
||||
|
||||
def ls( # pylint: disable=arguments-differ
|
||||
def ls( # pylint: disable=arguments-differ # noqa: C901
|
||||
self, path, detail=True, dvc_only=False, **kwargs
|
||||
):
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_exists = False
|
||||
dvc_infos = {}
|
||||
if dvc_fs:
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
with suppress(FileNotFoundError):
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
for info in dvc_fs.ls(dvc_path, detail=True):
|
||||
dvc_infos[dvc_fs.path.name(info["name"])] = info
|
||||
dvc_exists = bool(dvc_infos) or dvc_fs.exists(dvc_path)
|
||||
|
||||
fs_exists = False
|
||||
fs_infos = {}
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
if not dvc_only:
|
||||
@@ -289,13 +295,20 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
):
|
||||
fs_infos[fs.path.name(info["name"])] = info
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
pass # noqa: S110
|
||||
pass
|
||||
|
||||
fs_exists = bool(fs_infos) or fs.exists(fs_path)
|
||||
|
||||
dvcfiles = kwargs.get("dvcfiles", False)
|
||||
|
||||
infos = []
|
||||
paths = []
|
||||
names = set(dvc_infos.keys()) | set(fs_infos.keys())
|
||||
|
||||
if not names and (dvc_exists or fs_exists):
|
||||
# broken symlink or TreeError
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
||||
|
||||
for name in names:
|
||||
if not dvcfiles and _is_dvc_file(name):
|
||||
continue
|
||||
@@ -316,7 +329,9 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info(self, key, path, ignore_subrepos=True, check_ignored=True): # noqa: C901
|
||||
def _info( # noqa: C901, PLR0912
|
||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||
):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
dvc_info = None
|
||||
@@ -359,6 +374,19 @@ class _DVCFileSystem(AbstractFileSystem): # pylint:disable=abstract-method
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def get_file(self, rpath, lpath, **kwargs): # pylint: disable=arguments-differ
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
try:
|
||||
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
||||
except FileNotFoundError:
|
||||
_, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
if not dvc_fs:
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
|
||||
|
||||
class DVCFileSystem(FileSystem):
|
||||
protocol = "local"
|
||||
|
||||
+23
-11
@@ -19,7 +19,6 @@ SUBPROJECTS = (
|
||||
"dvc_objects",
|
||||
"dvc_render",
|
||||
"dvc_task",
|
||||
"dvclive",
|
||||
"scmrepo",
|
||||
)
|
||||
package = "" if PKG is None else f" ({PKG})"
|
||||
@@ -33,6 +32,7 @@ def get_dvc_info():
|
||||
f"Platform: Python {platform.python_version()} on {platform.platform()}",
|
||||
f"Subprojects:{_get_subprojects()}",
|
||||
f"Supports:{_get_supported_remotes()}",
|
||||
f"Config:{_get_config_dirs()}",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -41,20 +41,21 @@ def get_dvc_info():
|
||||
# can't auto-create it, as it might cause issues if the user
|
||||
# later decides to enable shared cache mode with
|
||||
# `dvc config cache.shared group`.
|
||||
if os.path.exists(repo.odb.local.path):
|
||||
if os.path.exists(repo.cache.local.path):
|
||||
info.append(f"Cache types: {_get_linktype_support_info(repo)}")
|
||||
fs_type = get_fs_type(repo.odb.local.path)
|
||||
fs_type = _get_fs_type(repo.cache.local.path)
|
||||
info.append(f"Cache directory: {fs_type}")
|
||||
else:
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
|
||||
info.append(f"Caches: {_get_caches(repo.odb)}")
|
||||
info.append(f"Caches: {_get_caches(repo.cache)}")
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = get_fs_type(os.path.abspath(root_directory))
|
||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||
info.append(f"Workspace directory: {fs_root}")
|
||||
info.append(f"Repo: {_get_dvc_repo_info(repo)}")
|
||||
info.append(f"Repo.site_cache_dir: {repo.site_cache_dir}")
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
except SCMError:
|
||||
@@ -84,7 +85,7 @@ def _get_remotes(config):
|
||||
|
||||
|
||||
def _get_linktype_support_info(repo):
|
||||
odb = repo.odb.local
|
||||
odb = repo.cache.local
|
||||
|
||||
links = generic.test_links(
|
||||
["reflink", "hardlink", "symlink"],
|
||||
@@ -136,10 +137,21 @@ def _get_supported_remotes():
|
||||
return "\n\t" + ",\n\t".join(supported_remotes)
|
||||
|
||||
|
||||
def get_fs_type(path):
|
||||
def _get_config_dirs():
|
||||
from dvc.config import Config
|
||||
|
||||
dirs = [
|
||||
f"Global: {Config.get_dir('global')}",
|
||||
f"System: {Config.get_dir('system')}",
|
||||
]
|
||||
|
||||
return "\n\t" + "\n\t".join(dirs)
|
||||
|
||||
|
||||
def _get_fs_type(path):
|
||||
partition = {}
|
||||
for part in psutil.disk_partitions(all=True):
|
||||
if part.fstype != "":
|
||||
if part.fstype:
|
||||
try:
|
||||
mountpoint = pathlib.Path(part.mountpoint).resolve()
|
||||
partition[mountpoint] = part.fstype + " on " + part.device
|
||||
@@ -155,11 +167,11 @@ def get_fs_type(path):
|
||||
return ("unknown", "none")
|
||||
|
||||
|
||||
def _get_dvc_repo_info(self):
|
||||
if self.config.get("core", {}).get("no_scm", False):
|
||||
def _get_dvc_repo_info(repo):
|
||||
if repo.config.get("core", {}).get("no_scm", False):
|
||||
return "dvc (no_scm)"
|
||||
|
||||
if self.root_dir != self.scm.root_dir:
|
||||
if repo.root_dir != repo.scm.root_dir:
|
||||
return "dvc (subdir), git"
|
||||
|
||||
return "dvc, git"
|
||||
|
||||
+3
-1
@@ -187,7 +187,9 @@ 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( # nosec B324, B303 # noqa: S324
|
||||
self._claimfile.encode()
|
||||
)
|
||||
filename = hasher.hexdigest()
|
||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||
|
||||
|
||||
+107
-171
@@ -1,63 +1,57 @@
|
||||
"""Manages logging configuration for DVC repo."""
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.env import DVC_SHOW_TRACEBACK
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
FOOTER = (
|
||||
"\n{yellow}Having any troubles?{nc}"
|
||||
" Hit us up at {blue}https://dvc.org/support{nc},"
|
||||
" we are always happy to help!"
|
||||
).format(
|
||||
blue=colorama.Fore.BLUE,
|
||||
nc=colorama.Fore.RESET,
|
||||
yellow=colorama.Fore.YELLOW,
|
||||
)
|
||||
|
||||
|
||||
def addLoggingLevel(levelName, levelNum, methodName=None):
|
||||
def add_logging_level(level_name, level_num, method_name=None):
|
||||
"""
|
||||
Adds a new logging level to the `logging` module and the
|
||||
currently configured logging class.
|
||||
|
||||
Uses the existing numeric levelNum if already defined.
|
||||
Uses the existing numeric level_num if already defined.
|
||||
|
||||
Based on https://stackoverflow.com/questions/2183233
|
||||
"""
|
||||
if methodName is None:
|
||||
methodName = levelName.lower()
|
||||
if method_name is None:
|
||||
method_name = level_name.lower()
|
||||
|
||||
# If the level name is already defined as a top-level `logging`
|
||||
# constant, then adopt the existing numeric level.
|
||||
if hasattr(logging, levelName):
|
||||
existingLevelNum = getattr(logging, levelName)
|
||||
assert isinstance(existingLevelNum, int)
|
||||
levelNum = existingLevelNum
|
||||
if hasattr(logging, level_name):
|
||||
existing_level_num = getattr(logging, level_name)
|
||||
assert isinstance(existing_level_num, int)
|
||||
level_num = existing_level_num
|
||||
|
||||
def logForLevel(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(levelNum):
|
||||
def log_for_level(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(level_num):
|
||||
# pylint: disable=protected-access
|
||||
self._log(levelNum, message, args, **kwargs)
|
||||
self._log(level_num, message, args, **kwargs)
|
||||
|
||||
def logToRoot(message, *args, **kwargs):
|
||||
logging.log(levelNum, message, *args, **kwargs)
|
||||
def log_to_root(message, *args, **kwargs):
|
||||
logging.log(level_num, message, *args, **kwargs)
|
||||
|
||||
# getLevelName resolves the numeric log level if already defined,
|
||||
# otherwise returns a string
|
||||
if not isinstance(logging.getLevelName(levelName), int):
|
||||
logging.addLevelName(levelNum, levelName)
|
||||
if not isinstance(logging.getLevelName(level_name), int):
|
||||
logging.addLevelName(level_num, level_name)
|
||||
|
||||
if not hasattr(logging, levelName):
|
||||
setattr(logging, levelName, levelNum)
|
||||
if not hasattr(logging, level_name):
|
||||
setattr(logging, level_name, level_num)
|
||||
|
||||
if not hasattr(logging.getLoggerClass(), methodName):
|
||||
setattr(logging.getLoggerClass(), methodName, logForLevel)
|
||||
if not hasattr(logging.getLoggerClass(), method_name):
|
||||
setattr(logging.getLoggerClass(), method_name, log_for_level)
|
||||
|
||||
if not hasattr(logging, methodName):
|
||||
setattr(logging, methodName, logToRoot)
|
||||
if not hasattr(logging, method_name):
|
||||
setattr(logging, method_name, log_to_root)
|
||||
|
||||
|
||||
class LoggingException(Exception):
|
||||
@@ -66,12 +60,11 @@ class LoggingException(Exception):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def excludeFilter(level):
|
||||
class ExcludeLevelFilter(logging.Filter):
|
||||
def filter(self, record): # noqa: A003
|
||||
return record.levelno < level
|
||||
def exclude_filter(level: int):
|
||||
def filter_fn(record: "logging.LogRecord") -> bool:
|
||||
return record.levelno < level
|
||||
|
||||
return ExcludeLevelFilter
|
||||
return filter_fn
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
@@ -86,7 +79,8 @@ class ColorFormatter(logging.Formatter):
|
||||
exception cause, just the message and the traceback.
|
||||
"""
|
||||
|
||||
color_code = {
|
||||
reset = colorama.Fore.RESET
|
||||
color_codes = {
|
||||
"TRACE": colorama.Fore.GREEN,
|
||||
"DEBUG": colorama.Fore.BLUE,
|
||||
"WARNING": colorama.Fore.YELLOW,
|
||||
@@ -94,51 +88,53 @@ class ColorFormatter(logging.Formatter):
|
||||
"CRITICAL": colorama.Fore.RED,
|
||||
}
|
||||
|
||||
def format(self, record): # noqa: A003
|
||||
def __init__(self, log_colors: bool = True, show_traceback: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.log_colors = log_colors
|
||||
self.show_traceback = show_traceback
|
||||
|
||||
def format(self, record) -> str: # noqa: A003, C901
|
||||
record.message = record.getMessage()
|
||||
msg = self.formatMessage(record)
|
||||
|
||||
if record.levelname == "INFO":
|
||||
if record.levelno == logging.INFO:
|
||||
return msg
|
||||
|
||||
if record.exc_info:
|
||||
if getattr(record, "tb_only", False):
|
||||
cause = ""
|
||||
else:
|
||||
cause = ": ".join(_iter_causes(record.exc_info[1]))
|
||||
ei = record.exc_info
|
||||
if ei:
|
||||
cause = ""
|
||||
if not getattr(record, "tb_only", False):
|
||||
cause = ": ".join(_iter_causes(ei[1]))
|
||||
sep = " - " if msg and cause else ""
|
||||
msg = msg + sep + cause
|
||||
|
||||
msg = "{message}{separator}{cause}".format(
|
||||
message=msg or "",
|
||||
separator=" - " if msg and cause else "",
|
||||
cause=cause,
|
||||
)
|
||||
asctime = ""
|
||||
verbose = _is_verbose()
|
||||
if verbose:
|
||||
asctime = self.formatTime(record, self.datefmt)
|
||||
if verbose or self.show_traceback:
|
||||
if ei and not record.exc_text:
|
||||
record.exc_text = self.formatException(ei)
|
||||
if record.exc_text:
|
||||
if msg[-1:] != "\n":
|
||||
msg = msg + "\n"
|
||||
msg = msg + record.exc_text + "\n"
|
||||
if record.stack_info:
|
||||
if msg[-1:] != "\n":
|
||||
msg = msg + "\n"
|
||||
msg = msg + self.formatStack(record.stack_info) + "\n"
|
||||
|
||||
if _is_verbose():
|
||||
msg += _stack_trace(record.exc_info)
|
||||
|
||||
return "{asctime}{color}{levelname}{nc}: {msg}".format(
|
||||
asctime=self.formatTime(record, self.datefmt),
|
||||
color=self.color_code[record.levelname],
|
||||
nc=colorama.Fore.RESET,
|
||||
levelname=record.levelname,
|
||||
msg=msg,
|
||||
)
|
||||
|
||||
def formatTime(self, record, datefmt=None):
|
||||
# only show if current level is set to DEBUG
|
||||
# also, skip INFO as it is used for UI
|
||||
if not _is_verbose() or record.levelno == logging.INFO:
|
||||
return ""
|
||||
|
||||
return "{green}{date}{nc} ".format(
|
||||
green=colorama.Fore.GREEN,
|
||||
date=super().formatTime(record, datefmt),
|
||||
nc=colorama.Fore.RESET,
|
||||
)
|
||||
level = record.levelname
|
||||
if self.log_colors:
|
||||
color = self.color_codes[level]
|
||||
if asctime:
|
||||
asctime = color + asctime + self.reset
|
||||
level = color + level + self.reset
|
||||
return asctime + (" " if asctime else "") + level + ": " + msg
|
||||
|
||||
|
||||
class LoggerHandler(logging.StreamHandler):
|
||||
def handleError(self, record):
|
||||
def handleError(self, record): # noqa: N802
|
||||
super().handleError(record)
|
||||
raise LoggingException(record)
|
||||
|
||||
@@ -155,8 +151,9 @@ class LoggerHandler(logging.StreamHandler):
|
||||
self.emit_pretty_exception(exc, verbose=_is_verbose())
|
||||
if not _is_verbose():
|
||||
return
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
pass # noqa: S110
|
||||
# pylint: disable-next=broad-except
|
||||
except Exception: # noqa: BLE001, S110 # nosec B110
|
||||
pass
|
||||
|
||||
msg = self.format(record)
|
||||
Tqdm.write(msg, file=self.stream, end=getattr(self, "terminator", "\n"))
|
||||
@@ -179,31 +176,47 @@ def _iter_causes(exc):
|
||||
exc = exc.__cause__
|
||||
|
||||
|
||||
def _stack_trace(exc_info):
|
||||
import traceback
|
||||
|
||||
return "\n{red}{line}{nc}\n{trace}{red}{line}{nc}".format(
|
||||
red=colorama.Fore.RED,
|
||||
line="-" * 60,
|
||||
trace="".join(traceback.format_exception(*exc_info)),
|
||||
nc=colorama.Fore.RESET,
|
||||
)
|
||||
|
||||
|
||||
def disable_other_loggers():
|
||||
logging.captureWarnings(True)
|
||||
loggerDict = logging.root.manager.loggerDict # pylint: disable=no-member
|
||||
for logger_name, logger in loggerDict.items():
|
||||
if logger_name != "dvc" and not logger_name.startswith("dvc."):
|
||||
logger.disabled = True # type: ignore[union-attr]
|
||||
|
||||
|
||||
def set_loggers_level(level: int = logging.INFO) -> None:
|
||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||
logging.getLogger(name).setLevel(level)
|
||||
|
||||
|
||||
def setup(level: int = logging.INFO) -> None:
|
||||
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
||||
colorama.init()
|
||||
|
||||
formatter = ColorFormatter(log_colors=log_colors and sys.stdout.isatty())
|
||||
|
||||
console_info = LoggerHandler(sys.stdout)
|
||||
console_info.setLevel(logging.INFO)
|
||||
console_info.setFormatter(formatter)
|
||||
console_info.addFilter(exclude_filter(logging.WARNING))
|
||||
|
||||
console_debug = LoggerHandler(sys.stdout)
|
||||
console_debug.setLevel(logging.DEBUG)
|
||||
console_debug.setFormatter(formatter)
|
||||
console_debug.addFilter(exclude_filter(logging.INFO))
|
||||
|
||||
add_logging_level("TRACE", logging.DEBUG - 5)
|
||||
|
||||
console_trace = LoggerHandler(sys.stdout)
|
||||
console_trace.setLevel(logging.TRACE) # type: ignore[attr-defined]
|
||||
console_trace.setFormatter(formatter)
|
||||
console_trace.addFilter(exclude_filter(logging.DEBUG))
|
||||
|
||||
show_traceback = bool(os.environ.get(DVC_SHOW_TRACEBACK))
|
||||
err_formatter = ColorFormatter(
|
||||
log_colors=log_colors and sys.stderr.isatty(), show_traceback=show_traceback
|
||||
)
|
||||
console_errors = LoggerHandler(sys.stderr)
|
||||
console_errors.setLevel(logging.WARNING)
|
||||
console_errors.setFormatter(err_formatter)
|
||||
|
||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
for handler in [console_info, console_debug, console_trace, console_errors]:
|
||||
logger.addHandler(handler)
|
||||
|
||||
if level >= logging.DEBUG:
|
||||
# Unclosed session errors for asyncio/aiohttp are only available
|
||||
# on the tracing mode for extensive debug purposes. They are really
|
||||
@@ -214,80 +227,3 @@ def setup(level: int = logging.INFO) -> None:
|
||||
# action.
|
||||
logging.getLogger("asyncio").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("aiohttp").setLevel(logging.CRITICAL)
|
||||
|
||||
addLoggingLevel("TRACE", logging.DEBUG - 5)
|
||||
logging.config.dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"filters": {
|
||||
"exclude_errors": {"()": excludeFilter(logging.WARNING)},
|
||||
"exclude_info": {"()": excludeFilter(logging.INFO)},
|
||||
"exclude_debug": {"()": excludeFilter(logging.DEBUG)},
|
||||
},
|
||||
"formatters": {"color": {"()": ColorFormatter}},
|
||||
"handlers": {
|
||||
"console_info": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "INFO",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_errors"],
|
||||
},
|
||||
"console_debug": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "DEBUG",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_info"],
|
||||
},
|
||||
"console_trace": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "TRACE",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_debug"],
|
||||
},
|
||||
"console_errors": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "WARNING",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stderr",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"dvc": {
|
||||
"level": level,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
"dvc_objects": {
|
||||
"level": level,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
"dvc_data": {
|
||||
"level": level,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
},
|
||||
"disable_existing_loggers": False,
|
||||
}
|
||||
)
|
||||
|
||||
# NOTE: colorama init needs to go after logging config to avoid potential
|
||||
# conflicts with libs that modify logging (like tensorflow/abseil)
|
||||
# https://github.com/iterative/dvc/issues/8387
|
||||
colorama.init()
|
||||
|
||||
+274
-99
@@ -1,7 +1,11 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import posixpath
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, cast
|
||||
from contextlib import suppress
|
||||
from operator import itemgetter
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import collecting, first, project
|
||||
@@ -18,21 +22,21 @@ from dvc.exceptions import (
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc_data.hashfile import Tree
|
||||
from dvc_data.hashfile import check as ocheck
|
||||
from dvc_data.hashfile import load as oload
|
||||
from dvc_data.hashfile.build import build
|
||||
from dvc_data.hashfile.checkout import checkout
|
||||
from dvc_data.hashfile.db import HashFileDB, add_update_tree
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
from dvc_data.hashfile.istextfile import istextfile
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
from dvc_data.hashfile.transfer import transfer as otransfer
|
||||
from dvc_data.index import DataIndexEntry
|
||||
from dvc_data.hashfile.tree import Tree, du
|
||||
from dvc_objects.errors import ObjectFormatError
|
||||
|
||||
from .annotations import ANNOTATION_FIELDS, ANNOTATION_SCHEMA, Annotation
|
||||
from .fs import LocalFileSystem, RemoteMissingDepsError, Schemes, get_cloud_fs
|
||||
from .fs.callbacks import DEFAULT_CALLBACK
|
||||
from .fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
|
||||
from .utils import relpath
|
||||
from .utils.fs import path_isin
|
||||
|
||||
@@ -41,7 +45,6 @@ if TYPE_CHECKING:
|
||||
from dvc_data.index import DataIndexKey
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .fs.callbacks import Callback
|
||||
from .ignore import DvcIgnoreFilter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -229,6 +232,17 @@ def merge_file_meta_from_cloud(entry: Dict) -> Dict:
|
||||
return entry
|
||||
|
||||
|
||||
def _serialize_tree_obj_to_files(obj: "Tree") -> List[Dict[str, Any]]:
|
||||
key = obj.PARAM_RELPATH
|
||||
return sorted(
|
||||
(
|
||||
{key: posixpath.sep.join(parts), **hi.to_dict(), **meta.to_dict()}
|
||||
for parts, meta, hi in obj
|
||||
),
|
||||
key=itemgetter(key),
|
||||
)
|
||||
|
||||
|
||||
class OutputDoesNotExistError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = f"output '{path}' does not exist"
|
||||
@@ -262,6 +276,11 @@ class OutputIsIgnoredError(DvcException):
|
||||
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
|
||||
|
||||
|
||||
class CheckoutCallback(TqdmCallback):
|
||||
# disable branching for checkouts
|
||||
branch = Callback.branch # type: ignore[assignment]
|
||||
|
||||
|
||||
class Output:
|
||||
IS_DEPENDENCY = False
|
||||
|
||||
@@ -299,7 +318,7 @@ class Output:
|
||||
IsStageFileError: Type[DvcException] = OutputIsStageFileError
|
||||
IsIgnoredError: Type[DvcException] = OutputIsIgnoredError
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
stage,
|
||||
path,
|
||||
@@ -381,6 +400,7 @@ class Output:
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj: Optional["HashFile"] = None
|
||||
|
||||
self.odb: Optional["HashFileDB"] = None
|
||||
self.remote = remote
|
||||
|
||||
if self.fs.version_aware:
|
||||
@@ -403,11 +423,14 @@ class Output:
|
||||
def _compute_meta_hash_info_from_files(self) -> None:
|
||||
if self.files:
|
||||
tree = Tree.from_list(self.files, hash_name=self.hash_name)
|
||||
tree.digest()
|
||||
tree.digest(with_meta=True)
|
||||
self.odb = HashFileDB(tree.fs, tree.path + ".odb")
|
||||
self.odb.add(tree.path, tree.fs, tree.hash_info.value)
|
||||
|
||||
self.hash_info = tree.hash_info
|
||||
self.meta.isdir = True
|
||||
self.meta.nfiles = len(self.files)
|
||||
self.meta.size = sum(f.get("size") for f in self.files)
|
||||
self.meta.size = sum(filter(None, (f.get("size") for f in self.files)))
|
||||
self.meta.remote = first(f.get("remote") for f in self.files)
|
||||
elif self.meta.nfiles or self.hash_info and self.hash_info.isdir:
|
||||
self.meta.isdir = True
|
||||
@@ -487,16 +510,18 @@ class Output:
|
||||
return self.use_cache or self.stage.is_repo_import
|
||||
|
||||
@property
|
||||
def odb(self):
|
||||
def cache(self):
|
||||
odb_name = "repo" if self.is_in_repo else self.protocol
|
||||
odb = getattr(self.repo.odb, odb_name)
|
||||
odb = getattr(self.repo.cache, odb_name)
|
||||
if self.use_cache and odb is None:
|
||||
raise RemoteCacheRequiredError(self.fs.protocol, self.fs_path)
|
||||
return odb
|
||||
|
||||
@property
|
||||
def cache_path(self):
|
||||
return self.odb.fs.unstrip_protocol(self.odb.oid_to_path(self.hash_info.value))
|
||||
return self.cache.fs.unstrip_protocol(
|
||||
self.cache.oid_to_path(self.hash_info.value)
|
||||
)
|
||||
|
||||
def get_hash(self):
|
||||
_, hash_info = self._get_hash_meta()
|
||||
@@ -504,9 +529,9 @@ class Output:
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
odb = self.odb
|
||||
odb = self.cache
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
odb = self.repo.cache.local
|
||||
_, meta, obj = build(
|
||||
odb,
|
||||
self.fs_path,
|
||||
@@ -548,26 +573,6 @@ class Output:
|
||||
key = self.fs.path.parts(no_drive)[1:]
|
||||
return workspace, key
|
||||
|
||||
def get_entry(self) -> "DataIndexEntry":
|
||||
if self.files and not self.obj:
|
||||
self.obj = self.get_obj()
|
||||
|
||||
entry = DataIndexEntry(
|
||||
meta=self.meta,
|
||||
obj=self.obj,
|
||||
hash_info=self.hash_info,
|
||||
)
|
||||
if self.stage.is_import and not self.stage.is_repo_import:
|
||||
dep = self.stage.deps[0]
|
||||
entry.fs = dep.fs
|
||||
entry.path = dep.fs_path
|
||||
entry.meta = dep.meta
|
||||
if not entry.obj:
|
||||
if not dep.obj and dep.files:
|
||||
dep.obj = dep.get_obj()
|
||||
entry.obj = dep.obj
|
||||
return entry
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.hash_info != self.get_hash()
|
||||
|
||||
@@ -580,7 +585,7 @@ class Output:
|
||||
return True
|
||||
|
||||
try:
|
||||
ocheck(self.odb, obj)
|
||||
ocheck(self.cache, obj)
|
||||
return False
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return True
|
||||
@@ -669,7 +674,7 @@ class Output:
|
||||
|
||||
if self.use_cache:
|
||||
_, self.meta, self.obj = build(
|
||||
self.odb,
|
||||
self.cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
@@ -677,7 +682,7 @@ class Output:
|
||||
)
|
||||
else:
|
||||
_, self.meta, self.obj = build(
|
||||
self.repo.odb.local,
|
||||
self.repo.cache.local,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
@@ -692,7 +697,7 @@ class Output:
|
||||
|
||||
def set_exec(self) -> None:
|
||||
if self.isfile() and self.meta.isexec:
|
||||
self.odb.set_exec(self.fs_path)
|
||||
self.cache.set_exec(self.fs_path)
|
||||
|
||||
def _checkout(self, *args, **kwargs) -> Optional[bool]:
|
||||
from dvc_data.hashfile.checkout import CheckoutError as _CheckoutError
|
||||
@@ -708,7 +713,7 @@ class Output:
|
||||
except _CheckoutError as exc:
|
||||
raise CheckoutError(exc.paths, {}) # noqa: B904
|
||||
|
||||
def commit(self, filter_info=None) -> None:
|
||||
def commit(self, filter_info=None, relink=True) -> None:
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self)
|
||||
|
||||
@@ -718,11 +723,14 @@ class Output:
|
||||
granular = (
|
||||
self.is_dir_checksum and filter_info and filter_info != self.fs_path
|
||||
)
|
||||
# NOTE: trying to use hardlink during transfer only if we will be
|
||||
# relinking later
|
||||
hardlink = relink
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info)
|
||||
obj = self._commit_granular_dir(filter_info, hardlink)
|
||||
else:
|
||||
staging, _, obj = build(
|
||||
self.odb,
|
||||
self.cache,
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
@@ -730,44 +738,49 @@ class Output:
|
||||
)
|
||||
otransfer(
|
||||
staging,
|
||||
self.odb,
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
shallow=False,
|
||||
hardlink=True,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.odb,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
)
|
||||
self.set_exec()
|
||||
if relink:
|
||||
rel = self.fs.path.relpath(filter_info or self.fs_path)
|
||||
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as cb:
|
||||
self._checkout(
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=cb,
|
||||
)
|
||||
self.set_exec()
|
||||
|
||||
def _commit_granular_dir(self, filter_info) -> Optional["HashFile"]:
|
||||
def _commit_granular_dir(self, filter_info, hardlink) -> Optional["HashFile"]:
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
staging, _, save_obj = build(
|
||||
self.odb,
|
||||
staging, _, obj = build(
|
||||
self.cache,
|
||||
self.fs_path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
)
|
||||
save_obj = cast(Tree, save_obj)
|
||||
save_obj = cast(Tree, save_obj.filter(prefix))
|
||||
checkout_obj = save_obj.get_obj(self.odb, prefix)
|
||||
assert isinstance(obj, Tree)
|
||||
save_obj = obj.filter(prefix)
|
||||
assert isinstance(save_obj, Tree)
|
||||
checkout_obj = save_obj.get_obj(self.cache, prefix)
|
||||
otransfer(
|
||||
staging,
|
||||
self.odb,
|
||||
self.cache,
|
||||
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
|
||||
shallow=True,
|
||||
hardlink=True,
|
||||
hardlink=hardlink,
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self, **kwargs): # noqa: C901
|
||||
def dumpd(self, **kwargs): # noqa: C901, PLR0912
|
||||
ret: Dict[str, Any] = {}
|
||||
with_files = (
|
||||
(not self.IS_DEPENDENCY or self.stage.is_import)
|
||||
@@ -821,9 +834,10 @@ class Output:
|
||||
if with_files:
|
||||
obj = self.obj or self.get_obj()
|
||||
if obj:
|
||||
obj = cast("Tree", obj)
|
||||
assert isinstance(obj, Tree)
|
||||
ret[self.PARAM_FILES] = [
|
||||
split_file_meta_from_cloud(f) for f in obj.as_list(with_meta=True)
|
||||
split_file_meta_from_cloud(f)
|
||||
for f in _serialize_tree_obj_to_files(obj)
|
||||
]
|
||||
return ret
|
||||
|
||||
@@ -858,7 +872,7 @@ class Output:
|
||||
obj = tree
|
||||
elif self.hash_info:
|
||||
try:
|
||||
obj = oload(self.odb, self.hash_info)
|
||||
obj = oload(self.cache, self.hash_info)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return None
|
||||
else:
|
||||
@@ -868,8 +882,8 @@ class Output:
|
||||
fs_path = self.fs.path
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
prefix = fs_path.relparts(filter_info, self.fs_path)
|
||||
obj = cast(Tree, obj)
|
||||
obj = obj.get_obj(self.odb, prefix)
|
||||
assert isinstance(obj, Tree)
|
||||
obj = obj.get_obj(self.cache, prefix)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -883,9 +897,16 @@ class Output:
|
||||
checkpoint_reset: bool = False,
|
||||
**kwargs,
|
||||
) -> Optional[Tuple[bool, Optional[bool]]]:
|
||||
# callback passed act as a aggregate callback.
|
||||
# do not let checkout to call set_size and change progressbar.
|
||||
class CallbackProxy(Callback):
|
||||
def relative_update(self, inc: int = 1) -> None:
|
||||
progress_callback.relative_update(inc)
|
||||
return super().relative_update(inc)
|
||||
|
||||
callback = CallbackProxy()
|
||||
if not self.use_cache:
|
||||
if progress_callback != DEFAULT_CALLBACK:
|
||||
progress_callback.relative_update(self.get_files_number(filter_info))
|
||||
callback.relative_update(self.get_files_number(filter_info))
|
||||
return None
|
||||
|
||||
obj = self.get_obj(filter_info=filter_info)
|
||||
@@ -905,9 +926,9 @@ class Output:
|
||||
filter_info or self.fs_path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.odb,
|
||||
self.cache,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
progress_callback=callback,
|
||||
relink=relink,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
@@ -946,7 +967,7 @@ class Output:
|
||||
self, source, odb=None, jobs=None, update=False, no_progress_bar=False
|
||||
):
|
||||
if odb is None:
|
||||
odb = self.odb
|
||||
odb = self.cache
|
||||
|
||||
cls, config, from_info = get_cloud_fs(self.repo, url=source)
|
||||
from_fs = cls(**config)
|
||||
@@ -998,29 +1019,33 @@ class Output:
|
||||
|
||||
def unprotect(self):
|
||||
if self.exists:
|
||||
self.odb.unprotect(self.fs_path)
|
||||
self.cache.unprotect(self.fs_path)
|
||||
|
||||
def get_dir_cache(self, **kwargs):
|
||||
def get_dir_cache(self, **kwargs) -> Optional["Tree"]:
|
||||
if not self.is_dir_checksum:
|
||||
raise DvcException("cannot get dir cache for file checksum")
|
||||
|
||||
obj = self.odb.get(self.hash_info.value)
|
||||
obj = self.cache.get(self.hash_info.value)
|
||||
try:
|
||||
ocheck(self.odb, obj)
|
||||
ocheck(self.cache, obj)
|
||||
except FileNotFoundError:
|
||||
if self.remote:
|
||||
kwargs["remote"] = self.remote
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
with suppress(Exception):
|
||||
self.repo.cloud.pull([obj.hash_info], **kwargs)
|
||||
|
||||
if self.obj:
|
||||
assert isinstance(self.obj, Tree)
|
||||
return self.obj
|
||||
|
||||
try:
|
||||
self.obj = oload(self.odb, self.hash_info)
|
||||
obj = oload(self.cache, self.hash_info)
|
||||
assert isinstance(obj, Tree)
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
self.obj = None
|
||||
obj = None
|
||||
|
||||
return self.obj
|
||||
self.obj = obj
|
||||
return obj
|
||||
|
||||
def _collect_used_dir_cache(
|
||||
self, remote=None, force=False, jobs=None, filter_info=None
|
||||
@@ -1035,7 +1060,7 @@ class Output:
|
||||
logger.debug("failed to pull cache for '%s'", self)
|
||||
|
||||
try:
|
||||
ocheck(self.odb, self.odb.get(self.hash_info.value))
|
||||
ocheck(self.cache, self.cache.get(self.hash_info.value))
|
||||
except FileNotFoundError:
|
||||
msg = (
|
||||
"Missing cache for directory '{}'. "
|
||||
@@ -1050,12 +1075,12 @@ class Output:
|
||||
return None
|
||||
|
||||
obj = self.get_obj()
|
||||
assert obj is None or isinstance(obj, Tree)
|
||||
if filter_info and filter_info != self.fs_path:
|
||||
assert obj
|
||||
prefix = self.fs.path.parts(self.fs.path.relpath(filter_info, self.fs_path))
|
||||
obj = cast(Tree, obj)
|
||||
return obj.filter(prefix)
|
||||
return cast(Tree, obj)
|
||||
return obj
|
||||
|
||||
def get_used_objs( # noqa: C901
|
||||
self, **kwargs
|
||||
@@ -1067,9 +1092,7 @@ class Output:
|
||||
|
||||
push: bool = kwargs.pop("push", False)
|
||||
if self.stage.is_repo_import:
|
||||
if push:
|
||||
return {}
|
||||
return self.get_used_external(**kwargs)
|
||||
return {}
|
||||
|
||||
if push and not self.can_push:
|
||||
return {}
|
||||
@@ -1099,7 +1122,7 @@ class Output:
|
||||
else:
|
||||
obj = self.get_obj(filter_info=kwargs.get("filter_info"))
|
||||
if not obj:
|
||||
obj = self.odb.get(self.hash_info.value)
|
||||
obj = self.cache.get(self.hash_info.value)
|
||||
|
||||
if not obj:
|
||||
return {}
|
||||
@@ -1121,15 +1144,6 @@ class Output:
|
||||
oids.add(oid)
|
||||
return oids
|
||||
|
||||
def get_used_external(
|
||||
self, **kwargs
|
||||
) -> Dict[Optional["ObjectDB"], Set["HashInfo"]]:
|
||||
if not self.use_cache or not self.stage.is_repo_import:
|
||||
return {}
|
||||
|
||||
(dep,) = self.stage.deps
|
||||
return dep.get_used_objs(**kwargs)
|
||||
|
||||
def _validate_output_path(self, path, stage=None):
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
|
||||
@@ -1167,7 +1181,7 @@ class Output:
|
||||
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
from dvc_data.hashfile.tree import MergeError as TreeMergeError
|
||||
from dvc_data.hashfile.tree import du, merge
|
||||
from dvc_data.hashfile.tree import merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1182,7 +1196,7 @@ class Output:
|
||||
|
||||
try:
|
||||
merged = merge(
|
||||
self.odb,
|
||||
self.cache,
|
||||
ancestor_info,
|
||||
self.hash_info,
|
||||
other.hash_info,
|
||||
@@ -1191,15 +1205,176 @@ class Output:
|
||||
except TreeMergeError as exc:
|
||||
raise MergeError(str(exc)) from exc
|
||||
|
||||
self.odb.add(merged.path, merged.fs, merged.oid)
|
||||
self.cache.add(merged.path, merged.fs, merged.oid)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.files = None
|
||||
self.meta = Meta(
|
||||
size=du(self.odb, merged),
|
||||
size=du(self.cache, merged),
|
||||
nfiles=len(merged),
|
||||
)
|
||||
|
||||
def unstage(self, path: str) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
if tree is None:
|
||||
raise DvcException(f"could not read {self.hash_info.value!r}")
|
||||
else:
|
||||
tree = Tree()
|
||||
|
||||
trie = tree.as_trie()
|
||||
assert isinstance(trie, Trie)
|
||||
|
||||
try:
|
||||
del trie[rel_key:] # type: ignore[misc]
|
||||
except KeyError:
|
||||
raise FileNotFoundError( # noqa: B904
|
||||
errno.ENOENT,
|
||||
os.strerror(errno.ENOENT),
|
||||
self.fs.path.relpath(path),
|
||||
)
|
||||
|
||||
new = tree.from_trie(trie)
|
||||
new.digest()
|
||||
return Meta(nfiles=len(new), isdir=True), new
|
||||
|
||||
def apply(
|
||||
self,
|
||||
path: str,
|
||||
obj: Union["Tree", "HashFile"],
|
||||
meta: "Meta",
|
||||
) -> Tuple["Meta", "Tree"]:
|
||||
from pygtrie import Trie
|
||||
|
||||
from dvc_objects.fs.path import Path
|
||||
|
||||
assert isinstance(self.fs.path, Path)
|
||||
append_only = True
|
||||
rel_key = tuple(self.fs.path.parts(self.fs.path.relpath(path, self.fs_path)))
|
||||
|
||||
if self.hash_info:
|
||||
tree = self.get_dir_cache()
|
||||
if tree is None:
|
||||
raise DvcException(f"could not read {self.hash_info.value!r}")
|
||||
else:
|
||||
tree = Tree()
|
||||
|
||||
trie = tree.as_trie()
|
||||
assert isinstance(trie, Trie)
|
||||
|
||||
try:
|
||||
del trie[rel_key:] # type: ignore[misc]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
append_only = False
|
||||
|
||||
items = {}
|
||||
if isinstance(obj, Tree):
|
||||
items = {(*rel_key, *key): (m, o) for key, m, o in obj}
|
||||
else:
|
||||
items = {rel_key: (meta, obj.hash_info)}
|
||||
trie.update(items)
|
||||
|
||||
new = Tree.from_trie(trie)
|
||||
new.digest()
|
||||
|
||||
size = self.meta.size if self.meta and self.meta.size else None
|
||||
if append_only and size and meta.size is not None:
|
||||
# if files were only appended, we can sum to the existing size
|
||||
size += meta.size
|
||||
elif self.hash_info and self.hash_info == new.hash_info:
|
||||
# if hashes are same, sizes must have been the same
|
||||
size = self.meta.size
|
||||
else:
|
||||
size = None
|
||||
|
||||
meta = Meta(nfiles=len(new), size=size, isdir=True)
|
||||
return meta, new
|
||||
|
||||
def add( # noqa: C901
|
||||
self, path: Optional[str] = None, no_commit: bool = False, relink: bool = True
|
||||
) -> Optional["HashFile"]:
|
||||
path = path or self.fs_path
|
||||
if self.hash_info and not self.is_dir_checksum and self.fs_path != path:
|
||||
raise DvcException(
|
||||
f"Cannot modify '{self}' which is being tracked as a file"
|
||||
)
|
||||
|
||||
assert self.repo
|
||||
cache = self.cache if self.use_cache else self.repo.cache.local
|
||||
assert isinstance(cache, HashFileDB)
|
||||
|
||||
new: "HashFile"
|
||||
try:
|
||||
assert self.hash_name
|
||||
staging, meta, obj = build(
|
||||
cache,
|
||||
path,
|
||||
self.fs,
|
||||
self.hash_name,
|
||||
ignore=self.dvcignore,
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
if self.fs_path == path:
|
||||
raise self.DoesNotExistError(self) from exc
|
||||
if not self.is_dir_checksum:
|
||||
raise
|
||||
|
||||
meta, new = self.unstage(path)
|
||||
staging, obj = None, None
|
||||
else:
|
||||
assert obj
|
||||
assert staging
|
||||
if self.fs_path != path:
|
||||
meta, new = self.apply(path, obj, meta)
|
||||
add_update_tree(staging, new)
|
||||
else:
|
||||
new = obj
|
||||
|
||||
self.obj = new
|
||||
self.hash_info = self.obj.hash_info
|
||||
self.meta = meta
|
||||
self.files = None
|
||||
self.ignore()
|
||||
|
||||
if no_commit or not self.use_cache:
|
||||
return obj
|
||||
|
||||
if isinstance(new, Tree):
|
||||
add_update_tree(cache, new)
|
||||
|
||||
if not obj:
|
||||
return obj
|
||||
|
||||
assert staging
|
||||
assert obj.hash_info
|
||||
otransfer(staging, self.cache, {obj.hash_info}, hardlink=relink, shallow=False)
|
||||
|
||||
if relink:
|
||||
rel = self.fs.path.relpath(path)
|
||||
with CheckoutCallback(desc=f"Checking out {rel}", unit="files") as callback:
|
||||
self._checkout(
|
||||
path,
|
||||
self.fs,
|
||||
obj,
|
||||
self.cache,
|
||||
relink=True,
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=callback,
|
||||
)
|
||||
self.set_exec()
|
||||
return obj
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.fs_path
|
||||
|
||||
@@ -392,7 +392,7 @@ class ForeachDefinition:
|
||||
"""Convert sequence to Mapping with keys normalized."""
|
||||
iterable = self.resolved_iterable
|
||||
if isinstance(iterable, Mapping):
|
||||
return iterable
|
||||
return {to_str(k): v for k, v in iterable.items()}
|
||||
|
||||
assert isinstance(iterable, Sequence)
|
||||
if any(map(is_map_or_seq, iterable)):
|
||||
|
||||
@@ -155,7 +155,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC): # noqa: B024
|
||||
class Container(Node, ABC):
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
|
||||
@@ -73,7 +73,7 @@ def embrace(s: str):
|
||||
|
||||
def escape_str(value):
|
||||
if os.name == "nt":
|
||||
from subprocess import list2cmdline
|
||||
from subprocess import list2cmdline # nosec B404
|
||||
|
||||
return list2cmdline([value])
|
||||
from shlex import quote
|
||||
@@ -95,16 +95,15 @@ def _(obj: bool):
|
||||
def _(obj: dict): # noqa: C901
|
||||
from dvc.config import Config
|
||||
|
||||
config = Config().get("parsing", {})
|
||||
config = Config.from_cwd().get("parsing", {})
|
||||
|
||||
result = ""
|
||||
for k, v in flatten(obj).items():
|
||||
if isinstance(v, bool):
|
||||
if v:
|
||||
result += f"--{k} "
|
||||
else:
|
||||
if config.get("bool", "store_true") == "boolean_optional":
|
||||
result += f"--no-{k} "
|
||||
elif config.get("bool", "store_true") == "boolean_optional":
|
||||
result += f"--no-{k} "
|
||||
|
||||
elif isinstance(v, str):
|
||||
result += f"--{k} {escape_str(v)} "
|
||||
|
||||
@@ -19,7 +19,7 @@ class VersionEnum(str, enum.Enum):
|
||||
return [v.value for v in cls]
|
||||
|
||||
|
||||
class LOCKFILE_VERSION(VersionEnum):
|
||||
class LOCKFILE_VERSION(VersionEnum): # noqa: N801
|
||||
V1 = "1.0"
|
||||
V2 = "2.0"
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class ProcessNotTerminatedError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' has not been terminated.")
|
||||
|
||||
|
||||
class ProcessNotFoundError(DvcException):
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Managed process '{name}' does not exist.")
|
||||
|
||||
|
||||
class TimeoutExpired(DvcException):
|
||||
def __init__(self, cmd, timeout):
|
||||
super().__init__(f"'{cmd}' did not complete before timeout '{timeout}'")
|
||||
self.cmd = cmd
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
class UnsupportedSignalError(DvcException):
|
||||
def __init__(self, sig):
|
||||
super().__init__(f"Unsupported signal: {sig}")
|
||||
@@ -1,149 +0,0 @@
|
||||
"""Serverless process manager."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Generator, List, Optional, Tuple, Union
|
||||
|
||||
from funcy.flow import reraise
|
||||
from shortuuid import uuid
|
||||
|
||||
from .exceptions import ProcessNotTerminatedError, UnsupportedSignalError
|
||||
from .process import ManagedProcess, ProcessInfo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProcessManager:
|
||||
"""Manager for controlling background ManagedProcess(es).
|
||||
|
||||
Spawned process entries are kept in the manager directory until they
|
||||
are explicitly removed (with remove() or cleanup()) so that return
|
||||
value and log information can be accessed after a process has completed.
|
||||
"""
|
||||
|
||||
def __init__(self, wdir: Optional[str] = None):
|
||||
self.wdir = wdir or "."
|
||||
|
||||
def __iter__(self) -> Generator[str, None, None]:
|
||||
if not os.path.exists(self.wdir):
|
||||
return
|
||||
yield from os.listdir(self.wdir)
|
||||
|
||||
def __getitem__(self, key: str) -> "ProcessInfo":
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
try:
|
||||
with open(info_path, encoding="utf-8") as fobj:
|
||||
return ProcessInfo.from_dict(json.load(fobj))
|
||||
except FileNotFoundError:
|
||||
raise KeyError # noqa: B904
|
||||
|
||||
@reraise(FileNotFoundError, KeyError)
|
||||
def __setitem__(self, key: str, value: "ProcessInfo"):
|
||||
info_path = os.path.join(self.wdir, key, f"{key}.json")
|
||||
with open(info_path, "w", encoding="utf-8") as fobj:
|
||||
return json.dump(value.asdict(), fobj)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
path = os.path.join(self.wdir, key)
|
||||
if os.path.exists(path):
|
||||
remove(path)
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def processes(self) -> Generator[Tuple[str, "ProcessInfo"], None, None]:
|
||||
for name in self:
|
||||
try:
|
||||
yield name, self[name]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
def spawn(self, args: Union[str, List[str]], name: Optional[str] = None):
|
||||
"""Run the given command in the background."""
|
||||
name = name or uuid()
|
||||
pid = ManagedProcess.spawn(
|
||||
args,
|
||||
wdir=os.path.join(self.wdir, name),
|
||||
name=name,
|
||||
)
|
||||
logger.debug(
|
||||
"Spawned managed process '%s' (PID: '%d')",
|
||||
name,
|
||||
pid,
|
||||
)
|
||||
|
||||
def send_signal(self, name: str, sig: int):
|
||||
"""Send `signal` to the specified named process."""
|
||||
process_info = self[name]
|
||||
if sys.platform == "win32" and sig not in (
|
||||
signal.SIGTERM,
|
||||
signal.CTRL_C_EVENT, # pylint: disable=no-member
|
||||
signal.CTRL_BREAK_EVENT, # pylint: disable=no-member
|
||||
):
|
||||
raise UnsupportedSignalError(sig)
|
||||
|
||||
def handle_closed_process():
|
||||
logging.warning("Process %s had already aborted unexpectedly.", name)
|
||||
process_info.returncode = -1
|
||||
self[name] = process_info
|
||||
|
||||
if process_info.returncode is None:
|
||||
try:
|
||||
os.kill(process_info.pid, sig)
|
||||
except ProcessLookupError:
|
||||
handle_closed_process()
|
||||
raise
|
||||
except OSError as exc:
|
||||
if getattr(exc, "winerror", None) == 87:
|
||||
handle_closed_process()
|
||||
raise ProcessLookupError from exc
|
||||
raise
|
||||
|
||||
def terminate(self, name: str):
|
||||
"""Terminate the specified named process."""
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
|
||||
def kill(self, name: str):
|
||||
"""Kill the specified named process."""
|
||||
if sys.platform == "win32":
|
||||
self.send_signal(name, signal.SIGTERM)
|
||||
else:
|
||||
self.send_signal(name, signal.SIGKILL) # pylint: disable=no-member
|
||||
|
||||
def remove(self, name: str, force: bool = False):
|
||||
"""Remove the specified named process from this manager.
|
||||
|
||||
If the specified process is still running, it will be forcefully killed
|
||||
if `force` is True`, otherwise an exception will be raised.
|
||||
|
||||
Raises:
|
||||
ProcessNotTerminatedError if the specified process is still
|
||||
running and was not forcefully killed.
|
||||
"""
|
||||
try:
|
||||
process_info = self[name]
|
||||
except KeyError:
|
||||
return
|
||||
if process_info.returncode is None and not force:
|
||||
raise ProcessNotTerminatedError(name)
|
||||
try:
|
||||
self.kill(name)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
del self[name]
|
||||
|
||||
def cleanup(self, force: bool = False):
|
||||
"""Remove stale (terminated) processes from this manager."""
|
||||
for name in self:
|
||||
try:
|
||||
self.remove(name, force)
|
||||
except ProcessNotTerminatedError:
|
||||
continue
|
||||
@@ -1,179 +0,0 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from contextlib import AbstractContextManager
|
||||
from dataclasses import asdict, dataclass
|
||||
from typing import BinaryIO, List, Optional, Union
|
||||
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
from .exceptions import TimeoutExpired
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: int
|
||||
stdin: Optional[str]
|
||||
stdout: Optional[str]
|
||||
stderr: Optional[str]
|
||||
returncode: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(**d)
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
|
||||
class ManagedProcess(AbstractContextManager):
|
||||
"""Run the specified command with redirected output.
|
||||
|
||||
stdout and stderr will both be redirected to <name>.out.
|
||||
Interactive processes (requiring stdin input) are currently unsupported.
|
||||
|
||||
Parameters:
|
||||
args: Command to be run.
|
||||
wdir: If specified, redirected output files will be placed in `wdir`.
|
||||
name: Name to use for this process, if not specified a UUID will be
|
||||
generated instead.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
args: Union[str, List[str]],
|
||||
wdir: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
):
|
||||
self.args: List[str] = (
|
||||
shlex.split(args, posix=os.name == "posix")
|
||||
if isinstance(args, str)
|
||||
else list(args)
|
||||
)
|
||||
self.wdir = wdir
|
||||
self.name = name or uuid()
|
||||
self.returncode: Optional[int] = None
|
||||
self._stdout: Optional[BinaryIO] = None
|
||||
self._stderr: Optional[BinaryIO] = None
|
||||
|
||||
self._run()
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.wait()
|
||||
|
||||
def _close_fds(self):
|
||||
if self._stdout:
|
||||
self._stdout.close()
|
||||
self._stdout = None
|
||||
if self._stderr:
|
||||
self._stderr.close()
|
||||
self._stderr = None
|
||||
|
||||
def _make_path(self, path: str) -> str:
|
||||
return os.path.join(self.wdir, path) if self.wdir else path
|
||||
|
||||
@cached_property
|
||||
def stdout_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.out")
|
||||
|
||||
@cached_property
|
||||
def info_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.json")
|
||||
|
||||
@cached_property
|
||||
def pidfile_path(self) -> str:
|
||||
return self._make_path(f"{self.name}.pid")
|
||||
|
||||
@property
|
||||
def info(self) -> "ProcessInfo":
|
||||
return ProcessInfo(
|
||||
pid=self.pid,
|
||||
stdin=None,
|
||||
stdout=self.stdout_path,
|
||||
stderr=None,
|
||||
returncode=self.returncode,
|
||||
)
|
||||
|
||||
def _make_wdir(self):
|
||||
if self.wdir:
|
||||
os.makedirs(self.wdir, exist_ok=True)
|
||||
|
||||
def _dump(self):
|
||||
self._make_wdir()
|
||||
with open(self.info_path, "w", encoding="utf-8") as fobj:
|
||||
json.dump(self.info.asdict(), fobj)
|
||||
with open(self.pidfile_path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(str(self.pid))
|
||||
|
||||
def _run(self) -> None:
|
||||
self._make_wdir()
|
||||
logger.debug(
|
||||
"Appending output to '%s'",
|
||||
self.stdout_path,
|
||||
)
|
||||
self._stdout = open(self.stdout_path, "ab") # noqa: SIM115
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
self.args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=self._stdout,
|
||||
stderr=subprocess.STDOUT,
|
||||
close_fds=True,
|
||||
shell=False,
|
||||
)
|
||||
self.pid: int = self._proc.pid
|
||||
self._dump()
|
||||
except Exception:
|
||||
if self._proc is not None:
|
||||
self._proc.kill()
|
||||
self._close_fds()
|
||||
raise
|
||||
|
||||
def wait(self, timeout: Optional[int] = None) -> Optional[int]:
|
||||
"""Block until a process started with `run` has completed.
|
||||
|
||||
Raises:
|
||||
TimeoutExpired if `timeout` was set and the process
|
||||
did not terminate after `timeout` seconds.
|
||||
"""
|
||||
if self.returncode is not None:
|
||||
return self.returncode
|
||||
try:
|
||||
self._proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise TimeoutExpired(exc.cmd, exc.timeout) from exc
|
||||
self.returncode = self._proc.returncode
|
||||
self._close_fds()
|
||||
self._dump()
|
||||
return self.returncode
|
||||
|
||||
@classmethod
|
||||
def spawn(cls, *args, **kwargs) -> Optional[int]:
|
||||
"""Spawn a ManagedProcess command in the background.
|
||||
|
||||
Returns: The spawned process PID.
|
||||
"""
|
||||
import multiprocessing as mp
|
||||
|
||||
proc = mp.Process(
|
||||
target=cls._spawn,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
# Do not terminate the child daemon when the main process exits
|
||||
# pylint: disable=protected-access
|
||||
mp.process._children.discard(proc) # type: ignore[attr-defined]
|
||||
return proc.pid
|
||||
|
||||
@classmethod
|
||||
def _spawn(cls, *args, **kwargs):
|
||||
with cls(*args, **kwargs):
|
||||
pass
|
||||
+1
-1
@@ -36,7 +36,7 @@ class Tqdm(tqdm):
|
||||
"miniters": 1,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0913
|
||||
self,
|
||||
iterable=None,
|
||||
disable=None,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Union
|
||||
|
||||
@@ -29,20 +28,20 @@ def _group_by_rev(datapoints):
|
||||
|
||||
|
||||
def to_json(renderer, split: bool = False) -> List[Dict]:
|
||||
from copy import deepcopy
|
||||
|
||||
if renderer.TYPE == "vega":
|
||||
grouped = _group_by_rev(deepcopy(renderer.datapoints))
|
||||
grouped = _group_by_rev(renderer.datapoints)
|
||||
if split:
|
||||
content = renderer.get_filled_template(skip_anchors=["data"])
|
||||
content = renderer.get_filled_template(
|
||||
skip_anchors=["data"], as_string=False
|
||||
)
|
||||
else:
|
||||
content = renderer.get_filled_template()
|
||||
content = renderer.get_filled_template(as_string=False)
|
||||
if grouped:
|
||||
return [
|
||||
{
|
||||
TYPE_KEY: renderer.TYPE,
|
||||
REVISIONS_KEY: sorted(grouped.keys()),
|
||||
"content": json.loads(content),
|
||||
"content": content,
|
||||
"datapoints": grouped,
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from copy import deepcopy
|
||||
import os
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from funcy import first, last
|
||||
@@ -56,7 +56,6 @@ def _verify_field(file2datapoints: Dict[str, List], filename: str, field: str):
|
||||
datapoint = first(file2datapoints[filename])
|
||||
if field not in datapoint:
|
||||
raise FieldNotFoundError(field, datapoint.keys())
|
||||
return
|
||||
|
||||
|
||||
def _get_xs(properties: Dict, file2datapoints: Dict[str, List[Dict]]):
|
||||
@@ -194,7 +193,7 @@ class VegaConverter(Converter):
|
||||
return first(fields)
|
||||
return "x"
|
||||
|
||||
def flat_datapoints(self, revision): # noqa: C901
|
||||
def flat_datapoints(self, revision): # noqa: C901, PLR0912
|
||||
file2datapoints, properties = self.convert()
|
||||
|
||||
props_update = {}
|
||||
@@ -222,7 +221,13 @@ class VegaConverter(Converter):
|
||||
)
|
||||
|
||||
all_datapoints = []
|
||||
all_y_fields = {y_field for _, y_field in ys}
|
||||
if ys:
|
||||
all_y_files, all_y_fields = list(zip(*ys))
|
||||
all_y_fields = set(all_y_fields)
|
||||
all_y_files = set(all_y_files)
|
||||
else:
|
||||
all_y_files = set()
|
||||
all_y_fields = set()
|
||||
|
||||
# override to unified y field name if there are different y fields
|
||||
if len(all_y_fields) > 1:
|
||||
@@ -230,10 +235,16 @@ class VegaConverter(Converter):
|
||||
else:
|
||||
props_update["y"] = first(all_y_fields)
|
||||
|
||||
# get common prefix to drop from file names
|
||||
if len(all_y_files) > 1:
|
||||
common_prefix_len = len(os.path.commonpath(all_y_files))
|
||||
else:
|
||||
common_prefix_len = 0
|
||||
|
||||
for i, (y_file, y_field) in enumerate(ys):
|
||||
if num_xs > 1:
|
||||
x_file, x_field = xs[i]
|
||||
datapoints = deepcopy(file2datapoints.get(y_file, []))
|
||||
datapoints = [{**d} for d in file2datapoints.get(y_file, [])]
|
||||
|
||||
if props_update.get("y", None) == "dvc_inferred_y_value":
|
||||
_update_from_field(
|
||||
@@ -259,12 +270,13 @@ class VegaConverter(Converter):
|
||||
"They have to have same length."
|
||||
)
|
||||
|
||||
y_file_short = y_file[common_prefix_len:].strip("/\\")
|
||||
_update_all(
|
||||
datapoints,
|
||||
update_dict={
|
||||
VERSION_FIELD: {
|
||||
"revision": revision,
|
||||
FILENAME_FIELD: y_file,
|
||||
FILENAME_FIELD: y_file_short,
|
||||
"field": y_field,
|
||||
}
|
||||
},
|
||||
|
||||
+39
-12
@@ -1,17 +1,20 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, DefaultDict, Dict, List, NamedTuple, Optional
|
||||
|
||||
import dpath
|
||||
import dpath.options
|
||||
import dpath.util
|
||||
from funcy import last
|
||||
from funcy import get_in, last
|
||||
|
||||
from dvc.repo.plots import infer_data_sources
|
||||
from dvc.repo.plots import _normpath, infer_data_sources
|
||||
from dvc.utils.plots import get_plot_id
|
||||
|
||||
from .convert import _get_converter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
from dvc_render.base import Renderer
|
||||
|
||||
|
||||
dpath.options.ALLOW_EMPTY_STRING_KEYS = True
|
||||
|
||||
@@ -43,35 +46,48 @@ class PlotsData:
|
||||
|
||||
def get_definition_data(self, target_files, rev):
|
||||
result = {}
|
||||
for file in target_files:
|
||||
for definition_file in target_files:
|
||||
if os.name == "nt":
|
||||
source_file = _normpath(definition_file).replace("\\", "/")
|
||||
else:
|
||||
source_file = definition_file
|
||||
file_content = (
|
||||
self.data.get(rev, {})
|
||||
.get("sources", {})
|
||||
.get("data", {})
|
||||
.get(file, {})
|
||||
.get(source_file, {})
|
||||
.get("data", {})
|
||||
)
|
||||
if file_content:
|
||||
result[file] = file_content
|
||||
|
||||
result[definition_file] = file_content
|
||||
return result
|
||||
|
||||
|
||||
def match_defs_renderers(
|
||||
class RendererWithErrors(NamedTuple):
|
||||
renderer: "Renderer"
|
||||
source_errors: Dict[str, Dict[str, Exception]]
|
||||
definition_errors: Dict[str, Exception]
|
||||
|
||||
|
||||
def match_defs_renderers( # noqa: C901, PLR0912
|
||||
data,
|
||||
out=None,
|
||||
templates_dir: Optional["StrPath"] = None,
|
||||
):
|
||||
) -> List[RendererWithErrors]:
|
||||
from dvc_render import ImageRenderer, VegaRenderer
|
||||
|
||||
plots_data = PlotsData(data)
|
||||
renderers = []
|
||||
renderer_cls = None
|
||||
|
||||
for plot_id, group in plots_data.group_definitions().items():
|
||||
plot_datapoints: List[Dict] = []
|
||||
props = _squash_plots_properties(group)
|
||||
final_props: Dict = {}
|
||||
|
||||
def_errors: Dict[str, Exception] = {}
|
||||
src_errors: DefaultDict[str, Dict[str, Exception]] = defaultdict(dict)
|
||||
|
||||
if out is not None:
|
||||
props["out"] = out
|
||||
if templates_dir is not None:
|
||||
@@ -90,13 +106,24 @@ def match_defs_renderers(
|
||||
|
||||
converter = _get_converter(renderer_cls, inner_id, props, definitions_data)
|
||||
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
for src in plot_sources:
|
||||
if error := get_in(data, [rev, "sources", "data", src, "error"]):
|
||||
src_errors[rev][src] = error
|
||||
|
||||
try:
|
||||
dps, rev_props = converter.flat_datapoints(rev)
|
||||
except Exception as e: # noqa: BLE001, pylint: disable=broad-except
|
||||
def_errors[rev] = e
|
||||
continue
|
||||
|
||||
if not final_props and rev_props:
|
||||
final_props = rev_props
|
||||
plot_datapoints.extend(dps)
|
||||
|
||||
if "title" not in final_props:
|
||||
final_props["title"] = renderer_id
|
||||
|
||||
if renderer_cls is not None:
|
||||
renderers.append(renderer_cls(plot_datapoints, renderer_id, **final_props))
|
||||
renderer = renderer_cls(plot_datapoints, renderer_id, **final_props)
|
||||
renderers.append(RendererWithErrors(renderer, dict(src_errors), def_errors))
|
||||
return renderers
|
||||
|
||||
+130
-100
@@ -3,10 +3,17 @@ import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Tuple, Union, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
ContextManager,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.exceptions import FileMissingError
|
||||
from dvc.exceptions import IsADirectoryError as DvcIsADirectoryError
|
||||
from dvc.exceptions import NotDvcRepoError, OutputNotFoundError
|
||||
from dvc.ignore import DvcIgnoreFilter
|
||||
from dvc.utils import env2bool
|
||||
@@ -14,7 +21,7 @@ from dvc.utils.fs import path_isin
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem, GitFileSystem
|
||||
from dvc.fs import FileSystem
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc.lock import LockBase
|
||||
@@ -23,6 +30,7 @@ if TYPE_CHECKING:
|
||||
from dvc.stage import Stage
|
||||
from dvc.types import DictStrAny
|
||||
from dvc_data.hashfile.state import StateBase
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
from .experiments import Experiments
|
||||
from .index import Index
|
||||
@@ -99,17 +107,15 @@ class Repo:
|
||||
fs: Optional["FileSystem"] = None,
|
||||
uninitialized: bool = False,
|
||||
scm: Optional[Union["Git", "NoSCM"]] = None,
|
||||
) -> Tuple[str, Optional[str], Optional[str]]:
|
||||
) -> Tuple[str, Optional[str]]:
|
||||
from dvc.fs import localfs
|
||||
from dvc.scm import SCM, SCMError
|
||||
|
||||
dvc_dir: Optional[str] = None
|
||||
tmp_dir: Optional[str] = None
|
||||
try:
|
||||
root_dir = self.find_root(root_dir, fs)
|
||||
fs = fs or localfs
|
||||
dvc_dir = fs.path.join(root_dir, self.DVC_DIR)
|
||||
tmp_dir = fs.path.join(dvc_dir, "tmp")
|
||||
except NotDvcRepoError:
|
||||
if not uninitialized:
|
||||
raise
|
||||
@@ -117,6 +123,8 @@ class Repo:
|
||||
if not scm:
|
||||
try:
|
||||
scm = SCM(root_dir or os.curdir)
|
||||
if scm.dulwich.repo.bare:
|
||||
raise NotDvcRepoError(f"{scm.root_dir} is a bare git repo")
|
||||
except SCMError:
|
||||
scm = SCM(os.curdir, no_scm=True)
|
||||
|
||||
@@ -124,33 +132,9 @@ class Repo:
|
||||
root_dir = scm.root_dir
|
||||
|
||||
assert root_dir
|
||||
return root_dir, dvc_dir, tmp_dir
|
||||
return root_dir, dvc_dir
|
||||
|
||||
def _get_database_dir(self, db_name: str) -> Optional[str]:
|
||||
# NOTE: by default, store SQLite-based remote indexes and state's
|
||||
# `links` and `md5s` caches in the repository itself to avoid any
|
||||
# possible state corruption in 'shared cache dir' scenario, but allow
|
||||
# user to override this through config when, say, the repository is
|
||||
# located on a mounted volume — see
|
||||
# https://github.com/iterative/dvc/issues/4420
|
||||
base_db_dir = self.config.get(db_name, {}).get("dir", None)
|
||||
if not base_db_dir:
|
||||
return self.tmp_dir
|
||||
|
||||
import hashlib
|
||||
|
||||
root_dir_hash = hashlib.sha224(self.root_dir.encode("utf-8")).hexdigest()
|
||||
|
||||
db_dir = self.fs.path.join(
|
||||
base_db_dir,
|
||||
self.DVC_DIR,
|
||||
f"{self.fs.path.name(self.root_dir)}-{root_dir_hash[0:7]}",
|
||||
)
|
||||
|
||||
self.fs.makedirs(db_dir, exist_ok=True)
|
||||
return db_dir
|
||||
|
||||
def __init__(
|
||||
def __init__( # noqa: PLR0915
|
||||
self,
|
||||
root_dir: Optional[str] = None,
|
||||
fs: Optional["FileSystem"] = None,
|
||||
@@ -162,11 +146,12 @@ class Repo:
|
||||
repo_factory: Optional[Callable] = None,
|
||||
scm: Optional[Union["Git", "NoSCM"]] = None,
|
||||
):
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc.config import Config
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.odbmgr import ODBManager
|
||||
from dvc.repo.artifacts import Artifacts
|
||||
from dvc.repo.metrics import Metrics
|
||||
from dvc.repo.params import Params
|
||||
from dvc.repo.plots import Plots
|
||||
@@ -179,6 +164,7 @@ class Repo:
|
||||
self._fs_conf = {"repo_factory": repo_factory}
|
||||
self._fs = fs or localfs
|
||||
self._scm = scm
|
||||
self._data_index = None
|
||||
|
||||
if rev and not fs:
|
||||
self._scm = scm = SCM(root_dir or os.curdir)
|
||||
@@ -187,8 +173,10 @@ class Repo:
|
||||
|
||||
self.root_dir: str
|
||||
self.dvc_dir: Optional[str]
|
||||
self.tmp_dir: Optional[str]
|
||||
self.root_dir, self.dvc_dir, self.tmp_dir = self._get_repo_dirs(
|
||||
(
|
||||
self.root_dir,
|
||||
self.dvc_dir,
|
||||
) = self._get_repo_dirs(
|
||||
root_dir=root_dir,
|
||||
fs=self.fs,
|
||||
uninitialized=uninitialized,
|
||||
@@ -205,30 +193,30 @@ class Repo:
|
||||
self.stage: "StageLoad" = StageLoad(self)
|
||||
|
||||
self.lock: "LockBase"
|
||||
self.odb: ODBManager
|
||||
self.cache: CacheManager
|
||||
self.state: "StateBase"
|
||||
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
self.odb = ODBManager(self)
|
||||
self.tmp_dir = None
|
||||
self.cache = CacheManager(self)
|
||||
else:
|
||||
self.fs.makedirs(cast(str, self.tmp_dir), exist_ok=True)
|
||||
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
assert self.tmp_dir
|
||||
self.fs.makedirs(self.tmp_dir, exist_ok=True)
|
||||
|
||||
self.lock = make_lock(
|
||||
self.fs.path.join(self.tmp_dir, "lock"),
|
||||
tmp_dir=self.tmp_dir,
|
||||
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
||||
friendly=True,
|
||||
)
|
||||
state_db_dir = self._get_database_dir("state")
|
||||
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
|
||||
os.makedirs(self.site_cache_dir, exist_ok=True)
|
||||
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
|
||||
else:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
|
||||
self.odb = ODBManager(self)
|
||||
self.cache = CacheManager(self)
|
||||
|
||||
self.stage_cache = StageCache(self)
|
||||
|
||||
@@ -237,6 +225,7 @@ class Repo:
|
||||
self.metrics: Metrics = Metrics(self)
|
||||
self.plots: Plots = Plots(self)
|
||||
self.params: Params = Params(self)
|
||||
self.artifacts: Artifacts = Artifacts(self)
|
||||
|
||||
self.stage_collection_error_handler: Optional[
|
||||
Callable[[str, Exception], None]
|
||||
@@ -246,6 +235,41 @@ class Repo:
|
||||
def __str__(self):
|
||||
return self.url or self.root_dir
|
||||
|
||||
@cached_property
|
||||
def local_dvc_dir(self):
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
if not self.dvc_dir:
|
||||
return None
|
||||
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
return self.dvc_dir
|
||||
|
||||
if not isinstance(self.fs, GitFileSystem):
|
||||
return None
|
||||
|
||||
relparts = ()
|
||||
if self.root_dir != "/":
|
||||
# subrepo
|
||||
relparts = self.fs.path.relparts(self.root_dir, "/")
|
||||
|
||||
dvc_dir = os.path.join(
|
||||
self.scm.root_dir,
|
||||
*relparts,
|
||||
self.DVC_DIR,
|
||||
)
|
||||
if os.path.exists(dvc_dir):
|
||||
return dvc_dir
|
||||
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def tmp_dir(self):
|
||||
if self.local_dvc_dir is None:
|
||||
return None
|
||||
|
||||
return os.path.join(self.local_dvc_dir, "tmp")
|
||||
|
||||
@cached_property
|
||||
def index(self) -> "Index":
|
||||
from dvc.repo.index import Index
|
||||
@@ -263,18 +287,9 @@ class Repo:
|
||||
|
||||
@staticmethod
|
||||
def open(url, *args, **kwargs): # noqa: A003
|
||||
if url is None:
|
||||
url = os.getcwd()
|
||||
from .open_repo import open_repo
|
||||
|
||||
if os.path.exists(url):
|
||||
try:
|
||||
return Repo(url, *args, **kwargs)
|
||||
except NotDvcRepoError:
|
||||
pass # fallthrough to external_repo
|
||||
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
return external_repo(url, *args, **kwargs)
|
||||
return open_repo(url, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def scm(self) -> Union["Git", "NoSCM"]:
|
||||
@@ -304,7 +319,7 @@ class Repo:
|
||||
return DvcIgnoreFilter(self.fs, self.root_dir)
|
||||
|
||||
def get_rev(self):
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
assert self.scm
|
||||
if isinstance(self.fs, LocalFileSystem):
|
||||
@@ -312,7 +327,8 @@ class Repo:
|
||||
|
||||
with map_scm_exception():
|
||||
return self.scm.get_rev()
|
||||
return cast("GitFileSystem", self.fs).rev
|
||||
assert isinstance(self.fs, GitFileSystem)
|
||||
return self.fs.rev
|
||||
|
||||
@cached_property
|
||||
def experiments(self) -> "Experiments":
|
||||
@@ -341,6 +357,17 @@ class Repo:
|
||||
# fs.
|
||||
self._reset()
|
||||
|
||||
@property
|
||||
def data_index(self) -> "DataIndex":
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
if self._data_index is None:
|
||||
index_dir = os.path.join(self.site_cache_dir, "index", "data")
|
||||
os.makedirs(index_dir, exist_ok=True)
|
||||
self._data_index = DataIndex.open(os.path.join(index_dir, "db.db"))
|
||||
|
||||
return self._data_index
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}: '{self.root_dir}'"
|
||||
|
||||
@@ -374,9 +401,12 @@ class Repo:
|
||||
raise NotDvcRepoError(msg)
|
||||
|
||||
@classmethod
|
||||
def find_dvc_dir(cls, root=None) -> str:
|
||||
root_dir = cls.find_root(root)
|
||||
return os.path.join(root_dir, cls.DVC_DIR)
|
||||
def find_dvc_dir(cls, root=None, fs=None) -> str:
|
||||
from dvc.fs import localfs
|
||||
|
||||
fs = fs or localfs
|
||||
root_dir = cls.find_root(root, fs=fs)
|
||||
return fs.path.join(root_dir, cls.DVC_DIR)
|
||||
|
||||
@staticmethod
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False, subdir=False) -> "Repo":
|
||||
@@ -385,14 +415,14 @@ class Repo:
|
||||
return init(root_dir=root_dir, no_scm=no_scm, force=force, subdir=subdir)
|
||||
|
||||
def unprotect(self, target):
|
||||
return self.odb.repo.unprotect(target)
|
||||
return self.cache.repo.unprotect(target)
|
||||
|
||||
def _ignore(self):
|
||||
flist = [self.config.files["local"]]
|
||||
if tmp_dir := self.tmp_dir:
|
||||
flist.append(tmp_dir)
|
||||
if path_isin(self.odb.repo.path, self.root_dir):
|
||||
flist.append(self.odb.repo.path)
|
||||
if path_isin(self.cache.repo.path, self.root_dir):
|
||||
flist.append(self.cache.repo.path)
|
||||
|
||||
for file in flist:
|
||||
self.scm_context.ignore(file)
|
||||
@@ -402,7 +432,12 @@ class Repo:
|
||||
|
||||
return brancher(self, *args, **kwargs)
|
||||
|
||||
def used_objs(
|
||||
def switch(self, rev: str) -> ContextManager[str]:
|
||||
from dvc.repo.brancher import switch
|
||||
|
||||
return switch(self, rev)
|
||||
|
||||
def used_objs( # noqa: PLR0913
|
||||
self,
|
||||
targets=None,
|
||||
all_branches=False,
|
||||
@@ -515,59 +550,54 @@ class Repo:
|
||||
return DVCFileSystem(repo=self, subrepos=self.subrepos, **self._fs_conf)
|
||||
|
||||
@cached_property
|
||||
def index_db_dir(self):
|
||||
return self._get_database_dir("index")
|
||||
def site_cache_dir(self) -> str:
|
||||
import getpass
|
||||
import hashlib
|
||||
|
||||
@contextmanager
|
||||
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
|
||||
"""Opens a specified resource as a file descriptor"""
|
||||
from dvc.fs.data import DataFileSystem
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
from dvc.dirs import site_cache_dir
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=self.index.data["local"])
|
||||
fs_path = path
|
||||
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
|
||||
|
||||
if isinstance(self.fs, GitFileSystem):
|
||||
relparts = ()
|
||||
if self.root_dir != "/":
|
||||
# subrepo
|
||||
relparts = self.fs.path.relparts(self.root_dir, "/")
|
||||
root_dir = os.path.join(self.scm.root_dir, *relparts)
|
||||
else:
|
||||
fs = DVCFileSystem(repo=self, subrepos=True)
|
||||
fs_path = fs.from_os_path(path)
|
||||
root_dir = self.root_dir
|
||||
|
||||
repos_dir = os.path.join(cache_dir, "repo")
|
||||
|
||||
umask = os.umask(0)
|
||||
try:
|
||||
if remote:
|
||||
remote_odb = self.cloud.get_remote_odb(name=remote)
|
||||
oid = fs.info(fs_path)["dvc_info"]["md5"]
|
||||
fs = remote_odb.fs
|
||||
fs_path = remote_odb.oid_to_path(oid)
|
||||
os.makedirs(repos_dir, mode=0o777, exist_ok=True)
|
||||
finally:
|
||||
os.umask(umask)
|
||||
|
||||
with fs.open(
|
||||
fs_path,
|
||||
mode=mode,
|
||||
encoding=encoding,
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError as exc:
|
||||
raise FileMissingError(path) from exc
|
||||
except IsADirectoryError as exc:
|
||||
raise DvcIsADirectoryError(f"'{path}' is a directory") from exc
|
||||
md5 = hashlib.md5( # noqa: S324 # nosec B324, B303
|
||||
str((root_dir, getpass.getuser())).encode()
|
||||
)
|
||||
repo_token = md5.hexdigest()
|
||||
return os.path.join(repos_dir, repo_token)
|
||||
|
||||
def close(self):
|
||||
self.scm.close()
|
||||
self.state.close()
|
||||
if self._data_index is not None:
|
||||
self._data_index.close()
|
||||
|
||||
def _reset_cached_indecies(self):
|
||||
def _reset(self):
|
||||
self.scm._reset() # pylint: disable=protected-access
|
||||
self.state.close()
|
||||
self.__dict__.pop("index", None)
|
||||
self.__dict__.pop("dvcignore", None)
|
||||
self.__dict__.pop("dvcfs", None)
|
||||
self.__dict__.pop("datafs", None)
|
||||
|
||||
def _reset(self):
|
||||
self.state.close()
|
||||
self.scm._reset() # pylint: disable=protected-access
|
||||
self._reset_cached_indecies()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self._reset()
|
||||
self.scm.close()
|
||||
self.close()
|
||||
|
||||
+63
-30
@@ -2,14 +2,16 @@ import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from itertools import tee
|
||||
from typing import TYPE_CHECKING, Any, Iterator, List, Optional
|
||||
from typing import TYPE_CHECKING, Any, Iterator, List, NamedTuple, Optional
|
||||
|
||||
import colorama
|
||||
|
||||
from dvc.exceptions import (
|
||||
CacheLinkError,
|
||||
DvcException,
|
||||
InvalidArgumentError,
|
||||
OutputDuplicationError,
|
||||
OutputNotFoundError,
|
||||
OverlappingOutputPathsError,
|
||||
RecursiveAddingWhileUsingFilename,
|
||||
)
|
||||
@@ -29,13 +31,25 @@ Stages = List["Stage"]
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
OVERLAPPING_OUTPUT_FMT = (
|
||||
class StageInfo(NamedTuple):
|
||||
stage: "Stage"
|
||||
output_exists: bool
|
||||
|
||||
|
||||
OVERLAPPING_CHILD_FMT = (
|
||||
"Cannot add '{out}', because it is overlapping with other "
|
||||
"DVC tracked output: '{parent}'.\n"
|
||||
"To include '{out}' in '{parent}', run "
|
||||
"'dvc commit {parent_stage}'"
|
||||
)
|
||||
|
||||
OVERLAPPING_PARENT_FMT = (
|
||||
"Cannot add '{parent}', because it is overlapping with other "
|
||||
"DVC tracked output: '{out}'.\n"
|
||||
"To include '{out}' in '{parent}', run "
|
||||
"'dvc remove {out_stage}' and then 'dvc add {parent}'"
|
||||
)
|
||||
|
||||
|
||||
def check_recursive_and_fname(args):
|
||||
if args.recursive and args.fname:
|
||||
@@ -86,11 +100,18 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
try:
|
||||
yield
|
||||
except OverlappingOutputPathsError as exc:
|
||||
msg = OVERLAPPING_OUTPUT_FMT.format(
|
||||
out=exc.overlapping_out.fs_path,
|
||||
parent=exc.parent.fs_path,
|
||||
parent_stage=exc.parent.stage.addressing,
|
||||
)
|
||||
if exc.parent in [o for s in stages for o in s.outs]:
|
||||
msg = OVERLAPPING_PARENT_FMT.format(
|
||||
out=exc.overlapping_out,
|
||||
parent=exc.parent,
|
||||
out_stage=exc.overlapping_out.stage.addressing,
|
||||
)
|
||||
else:
|
||||
msg = OVERLAPPING_CHILD_FMT.format(
|
||||
out=exc.overlapping_out,
|
||||
parent=exc.parent,
|
||||
parent_stage=exc.parent.stage.addressing,
|
||||
)
|
||||
raise OverlappingOutputPathsError( # noqa: B904
|
||||
exc.parent, exc.overlapping_out, msg
|
||||
)
|
||||
@@ -100,7 +121,7 @@ def translate_graph_error(stages: Stages) -> Iterator[None]:
|
||||
)
|
||||
|
||||
|
||||
def progress_iter(stages: Stages) -> Iterator["Stage"]:
|
||||
def progress_iter(stages: List[StageInfo]) -> Iterator["StageInfo"]:
|
||||
total = len(stages)
|
||||
desc = "Adding..."
|
||||
with ui.progress(stages, total=total, desc=desc, unit="file", leave=True) as pbar:
|
||||
@@ -108,10 +129,10 @@ def progress_iter(stages: Stages) -> Iterator["Stage"]:
|
||||
pbar.bar_format = desc
|
||||
pbar.refresh()
|
||||
|
||||
for stage in pbar:
|
||||
for item in pbar:
|
||||
if total > 1:
|
||||
pbar.set_msg(f"{stage.outs[0]}")
|
||||
yield stage
|
||||
pbar.set_msg(f"{item.stage.outs[0]}")
|
||||
yield item
|
||||
if total == 1: # restore bar format for stats
|
||||
# pylint: disable=no-member
|
||||
pbar.bar_format = pbar.BAR_FMT_DEFAULT
|
||||
@@ -148,7 +169,7 @@ VALIDATORS = (
|
||||
@validate(*VALIDATORS)
|
||||
@locked
|
||||
@scm_context
|
||||
def add( # noqa: C901
|
||||
def add(
|
||||
repo: "Repo",
|
||||
targets: "TargetType",
|
||||
recursive: bool = False,
|
||||
@@ -169,12 +190,14 @@ def add( # noqa: C901
|
||||
desc = "Collecting targets"
|
||||
stages_it = create_stages(repo, add_targets, fname, transfer, **kwargs)
|
||||
stages = list(ui.progress(stages_it, desc=desc, unit="file"))
|
||||
|
||||
stages_list = [stage for stage, _ in stages]
|
||||
msg = "Collecting stages from the workspace"
|
||||
with translate_graph_error(stages), ui.status(msg) as status:
|
||||
with translate_graph_error(stages_list), ui.status(msg) as status:
|
||||
# remove existing stages that are to-be replaced with these
|
||||
# new stages for the graph checks.
|
||||
repo.check_graph(
|
||||
stages=stages, callback=lambda: status.update("Checking graph")
|
||||
stages=stages_list, callback=lambda: status.update("Checking graph")
|
||||
)
|
||||
|
||||
odb = None
|
||||
@@ -182,18 +205,18 @@ def add( # noqa: C901
|
||||
odb = repo.cloud.get_remote_odb(kwargs.get("remote"), "add")
|
||||
|
||||
with warn_link_failures() as link_failures:
|
||||
for stage, source in zip(progress_iter(stages), sources):
|
||||
for (stage, output_exists), source in zip(progress_iter(stages), sources):
|
||||
out = stage.outs[0]
|
||||
if to_remote or to_cache:
|
||||
stage.transfer(source, to_remote=to_remote, odb=odb, **kwargs)
|
||||
else:
|
||||
try:
|
||||
stage.save()
|
||||
if not no_commit:
|
||||
stage.commit()
|
||||
path = out.fs.path.abspath(source) if output_exists else None
|
||||
stage.add_outs(path, no_commit=no_commit)
|
||||
except CacheLinkError:
|
||||
link_failures.append(str(stage.relpath))
|
||||
stage.dump()
|
||||
return stages
|
||||
return stages_list
|
||||
|
||||
|
||||
LARGE_DIR_RECURSIVE_ADD_WARNING = (
|
||||
@@ -247,24 +270,34 @@ def create_stages(
|
||||
fname: Optional[str] = None,
|
||||
transfer: bool = False,
|
||||
external: bool = False,
|
||||
force: bool = False,
|
||||
**kwargs: Any,
|
||||
) -> Iterator["Stage"]:
|
||||
) -> Iterator[StageInfo]:
|
||||
for target in targets:
|
||||
if kwargs.get("out"):
|
||||
target = resolve_output(target, kwargs["out"])
|
||||
target = resolve_output(target, kwargs["out"], force=force)
|
||||
path, wdir, out = resolve_paths(
|
||||
repo, target, always_local=transfer and not kwargs.get("out")
|
||||
)
|
||||
|
||||
stage = repo.stage.create(
|
||||
single_stage=True,
|
||||
validate=False,
|
||||
fname=fname or path,
|
||||
wdir=wdir,
|
||||
outs=[out],
|
||||
external=external,
|
||||
)
|
||||
try:
|
||||
(out_obj,) = repo.find_outs_by_path(target, strict=False)
|
||||
stage = out_obj.stage
|
||||
if not stage.is_data_source:
|
||||
raise DvcException(f"cannot update {out!r}: not a data source")
|
||||
output_exists = True
|
||||
except OutputNotFoundError:
|
||||
stage = repo.stage.create(
|
||||
single_stage=True,
|
||||
validate=False,
|
||||
fname=fname or path,
|
||||
wdir=wdir,
|
||||
outs=[out],
|
||||
external=external,
|
||||
force=force,
|
||||
)
|
||||
output_exists = False
|
||||
|
||||
out_obj = stage.outs[0]
|
||||
out_obj.annot.update(**kwargs)
|
||||
yield stage
|
||||
yield StageInfo(stage, output_exists)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import logging
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from dvc.annotations import Artifact
|
||||
from dvc.dvcfile import PROJECT_FILE
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.repo import Repo
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.serialize import modify_yaml
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Constants are taken from GTO.
|
||||
# When we make it a dependency, we can import them instead
|
||||
SEPARATOR_IN_NAME = ":"
|
||||
DIRNAME = r"[a-z0-9-_./]+"
|
||||
NAME = r"[a-z0-9]([a-z0-9-/]*[a-z0-9])?"
|
||||
NAME_RE = re.compile(f"^{NAME}$")
|
||||
FULLNAME = f"((?P<dirname>{DIRNAME}){SEPARATOR_IN_NAME})?(?P<name>{NAME})"
|
||||
FULLNAME_RE = re.compile(f"^{FULLNAME}$")
|
||||
|
||||
|
||||
def name_is_compatible(name: str) -> bool:
|
||||
return bool(NAME_RE.search(name))
|
||||
|
||||
|
||||
def check_name_format(name: str) -> None:
|
||||
if not name_is_compatible(name):
|
||||
raise InvalidArgumentError(
|
||||
f"Can't use '{name}' as artifact name (ID)."
|
||||
" You can use letters and numbers, and use '-' as separator"
|
||||
" (but not at the start or end)."
|
||||
)
|
||||
|
||||
|
||||
def check_for_nested_dvc_repo(dvcfile: Path):
|
||||
if dvcfile.is_absolute():
|
||||
raise InvalidArgumentError("Use relative path to dvc.yaml.")
|
||||
path = dvcfile.parent
|
||||
while path.name:
|
||||
if (path / Repo.DVC_DIR).is_dir():
|
||||
raise InvalidArgumentError(
|
||||
f"Nested DVC repos like {path} are not supported."
|
||||
)
|
||||
path = path.parent
|
||||
|
||||
|
||||
class Artifacts:
|
||||
def __init__(self, repo: "Repo") -> None:
|
||||
self.repo = repo
|
||||
|
||||
def read(self) -> Dict[str, Dict[str, Artifact]]:
|
||||
artifacts: Dict[str, Dict[str, Artifact]] = {}
|
||||
for (
|
||||
dvcfile,
|
||||
dvcfile_artifacts,
|
||||
) in self.repo.index._artifacts.items(): # pylint: disable=protected-access
|
||||
dvcyaml = relpath(dvcfile, self.repo.root_dir)
|
||||
artifacts[dvcyaml] = {}
|
||||
for name, value in dvcfile_artifacts.items():
|
||||
try:
|
||||
check_name_format(name)
|
||||
except InvalidArgumentError as e:
|
||||
logger.warning(e.msg)
|
||||
artifacts[dvcyaml][name] = Artifact(**value)
|
||||
return artifacts
|
||||
|
||||
def add(self, name: str, artifact: Artifact, dvcfile: Optional[str] = None):
|
||||
with self.repo.scm_context(quiet=True):
|
||||
check_name_format(name)
|
||||
dvcyaml = Path(dvcfile or PROJECT_FILE)
|
||||
check_for_nested_dvc_repo(
|
||||
dvcyaml.relative_to(self.repo.root_dir)
|
||||
if dvcyaml.is_absolute()
|
||||
else dvcyaml
|
||||
)
|
||||
|
||||
with modify_yaml(dvcyaml) as data:
|
||||
artifacts = data.setdefault("artifacts", {})
|
||||
artifacts.update({name: artifact.to_dict()})
|
||||
|
||||
self.repo.scm_context.track_file(dvcfile)
|
||||
|
||||
return artifacts.get(name)
|
||||
+72
-14
@@ -1,7 +1,17 @@
|
||||
from typing import Optional
|
||||
import logging
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Iterator, Optional, Tuple
|
||||
|
||||
from scmrepo.git import Git
|
||||
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.scm import iter_revs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def brancher( # noqa: E302
|
||||
self,
|
||||
@@ -60,6 +70,7 @@ def brancher( # noqa: E302
|
||||
|
||||
scm = self.scm
|
||||
|
||||
logger.trace("switching fs to workspace") # type: ignore[attr-defined]
|
||||
self.fs = LocalFileSystem(url=self.root_dir)
|
||||
yield "workspace"
|
||||
|
||||
@@ -79,21 +90,68 @@ def brancher( # noqa: E302
|
||||
)
|
||||
|
||||
try:
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
for sha, names in found_revs.items():
|
||||
self._reset_cached_indecies() # pylint: disable=W0212
|
||||
self.fs = GitFileSystem(scm=scm, rev=sha)
|
||||
self.root_dir = self.fs.path.join("/", *repo_root_parts)
|
||||
|
||||
if cwd_parts:
|
||||
cwd = self.fs.path.join("/", *cwd_parts) # type: ignore[unreachable]
|
||||
self.fs.path.chdir(cwd)
|
||||
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
if self.fs.exists(self.root_dir):
|
||||
try:
|
||||
_switch_fs(self, sha, repo_root_parts, cwd_parts)
|
||||
yield sha if sha_only else ",".join(names)
|
||||
except NotDvcRepoError:
|
||||
# ignore revs that don't contain repo root
|
||||
# (i.e. revs from before a subdir=True repo was init'ed)
|
||||
pass
|
||||
finally:
|
||||
self.fs = saved_fs
|
||||
self.root_dir = saved_root
|
||||
|
||||
|
||||
def _switch_fs(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
repo_root_parts: Tuple[str, ...],
|
||||
cwd_parts: Tuple[str, ...],
|
||||
):
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
|
||||
if rev == "workspace":
|
||||
logger.trace("switching fs to workspace") # type: ignore[attr-defined]
|
||||
repo.fs = LocalFileSystem(url=repo.root_dir)
|
||||
return
|
||||
|
||||
logger.trace("switching fs to revision %s", rev[:7]) # type: ignore[attr-defined]
|
||||
assert isinstance(repo.scm, Git)
|
||||
fs = GitFileSystem(scm=repo.scm, rev=rev)
|
||||
root_dir = repo.fs.path.join("/", *repo_root_parts)
|
||||
if not fs.exists(root_dir):
|
||||
raise NotDvcRepoError(f"Commit '{rev[:7]}' does not contain a DVC repo")
|
||||
|
||||
repo.fs = fs
|
||||
repo.root_dir = root_dir
|
||||
|
||||
if cwd_parts:
|
||||
cwd = repo.fs.path.join("/", *cwd_parts)
|
||||
repo.fs.path.chdir(cwd)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def switch(repo: "Repo", rev: str) -> Iterator[str]:
|
||||
"""Switch to a specific revision."""
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if rev != "workspace":
|
||||
rev = resolve_rev(repo.scm, rev)
|
||||
|
||||
repo_root_parts = ()
|
||||
if repo.fs.path.isin(repo.root_dir, repo.scm.root_dir):
|
||||
repo_root_parts = repo.fs.path.relparts(repo.root_dir, repo.scm.root_dir)
|
||||
|
||||
cwd_parts = ()
|
||||
if repo.fs.path.isin(repo.fs.path.getcwd(), repo.scm.root_dir):
|
||||
cwd_parts = repo.fs.path.relparts(repo.fs.path.getcwd(), repo.scm.root_dir)
|
||||
|
||||
saved_fs = repo.fs
|
||||
saved_root = repo.root_dir
|
||||
try:
|
||||
_switch_fs(repo, rev, repo_root_parts, cwd_parts)
|
||||
yield rev
|
||||
finally:
|
||||
repo.fs = saved_fs
|
||||
repo.root_dir = saved_root
|
||||
|
||||
+6
-1
@@ -43,6 +43,7 @@ def commit(
|
||||
force=False,
|
||||
allow_missing=False,
|
||||
data_only=False,
|
||||
relink=True,
|
||||
):
|
||||
stages_info = [
|
||||
info
|
||||
@@ -60,6 +61,10 @@ def commit(
|
||||
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)
|
||||
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]
|
||||
|
||||
+51
-22
@@ -13,17 +13,20 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.index import DataIndex
|
||||
from dvc_data.index.diff import Change
|
||||
|
||||
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _adapt_typ(typ):
|
||||
def _adapt_typ(typ: str) -> str:
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY
|
||||
|
||||
if typ == MODIFY:
|
||||
@@ -38,7 +41,7 @@ def _adapt_typ(typ):
|
||||
return typ
|
||||
|
||||
|
||||
def _adapt_path(change):
|
||||
def _adapt_path(change: "Change") -> str:
|
||||
isdir = False
|
||||
if change.new and change.new.meta:
|
||||
isdir = change.new.meta.isdir
|
||||
@@ -54,9 +57,12 @@ def _diff(
|
||||
old: "DataIndex",
|
||||
new: "DataIndex",
|
||||
*,
|
||||
granular: Optional[bool] = False,
|
||||
with_missing: Optional[bool] = False,
|
||||
granular: bool = False,
|
||||
not_in_cache: bool = False,
|
||||
not_in_remote: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
) -> Dict[str, List[str]]:
|
||||
from dvc_data.index import StorageError
|
||||
from dvc_data.index.diff import UNCHANGED, UNKNOWN, diff
|
||||
|
||||
ret: Dict[str, List[str]] = {}
|
||||
@@ -91,14 +97,27 @@ def _diff(
|
||||
continue
|
||||
|
||||
if (
|
||||
with_missing
|
||||
not_in_cache
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.odb_map[change.key].exists(change.old.hash_info.value)
|
||||
and not old.storage_map.cache_exists(change.old)
|
||||
):
|
||||
# NOTE: emulating previous behaviour
|
||||
_add_change("not_in_cache", change)
|
||||
|
||||
try:
|
||||
if (
|
||||
not_in_remote
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.storage_map.remote_exists(
|
||||
change.old, refresh=remote_refresh
|
||||
)
|
||||
):
|
||||
_add_change("not_in_remote", change)
|
||||
except StorageError:
|
||||
pass
|
||||
|
||||
_add_change(change.typ, change)
|
||||
|
||||
return ret
|
||||
@@ -143,14 +162,18 @@ def _git_info(scm: Union["Git", "NoSCM"], untracked_files: str = "all") -> GitIn
|
||||
def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> Dict[str, List[str]]:
|
||||
from .index import build_data_index
|
||||
|
||||
workspace = build_data_index(repo.index, repo.root_dir, repo.fs, compute_hash=True)
|
||||
with ui.status("Building workspace index"):
|
||||
workspace = build_data_index(
|
||||
repo.index, repo.root_dir, repo.fs, compute_hash=True
|
||||
)
|
||||
|
||||
return _diff(
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
with_missing=True,
|
||||
**kwargs,
|
||||
)
|
||||
with ui.status("Calculating diff between index/workspace"):
|
||||
return _diff(
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
not_in_cache=True,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _diff_head_to_index(
|
||||
@@ -158,17 +181,16 @@ def _diff_head_to_index(
|
||||
) -> Dict[str, List[str]]:
|
||||
index = repo.index.data["repo"]
|
||||
|
||||
for rev in repo.brancher(revs=[head]):
|
||||
if rev == "workspace":
|
||||
continue
|
||||
|
||||
with repo.switch(head):
|
||||
head_index = repo.index.data["repo"]
|
||||
|
||||
return _diff(head_index, index, **kwargs)
|
||||
with ui.status("Calculating diff between head/index"):
|
||||
return _diff(head_index, index, **kwargs)
|
||||
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: List[str]
|
||||
not_in_remote: List[str]
|
||||
committed: Dict[str, List[str]]
|
||||
uncommitted: Dict[str, List[str]]
|
||||
untracked: List[str]
|
||||
@@ -195,12 +217,18 @@ def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]) -> List[str]
|
||||
return [repo.fs.path.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
def status(
|
||||
repo: "Repo",
|
||||
untracked_files: str = "no",
|
||||
**kwargs: Any,
|
||||
) -> Status:
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
not_in_cache = uncommitted_diff.pop("not_in_cache", [])
|
||||
uncommitted_diff = _diff_index_to_wtree(
|
||||
repo,
|
||||
**kwargs,
|
||||
)
|
||||
unchanged = set(uncommitted_diff.pop("unchanged", []))
|
||||
|
||||
try:
|
||||
@@ -215,7 +243,8 @@ def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
untracked = _transform_git_paths_to_dvc(repo, untracked)
|
||||
# order matters here
|
||||
return Status(
|
||||
not_in_cache=not_in_cache,
|
||||
not_in_cache=uncommitted_diff.pop("not_in_cache", []),
|
||||
not_in_remote=uncommitted_diff.pop("not_in_remote", []),
|
||||
committed=committed_diff,
|
||||
uncommitted=uncommitted_diff,
|
||||
untracked=untracked,
|
||||
|
||||
+120
-229
@@ -1,39 +1,103 @@
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from dvc.exceptions import PathMissingError
|
||||
from dvc.repo import locked
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import DVCFileSystem, FileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.hashfile.tree import Tree
|
||||
from dvc.ui import ui
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _path(entry):
|
||||
if entry and entry.meta and entry.meta.isdir:
|
||||
return os.path.join(*entry.key, "")
|
||||
return os.path.join(*entry.key)
|
||||
|
||||
|
||||
def _hash(entry):
|
||||
if entry and entry.hash_info:
|
||||
return entry.hash_info.value
|
||||
return None
|
||||
|
||||
|
||||
def _diff(old, new, with_missing=False):
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY, RENAME
|
||||
from dvc_data.index.diff import diff as idiff
|
||||
|
||||
ret: "Dict[str, List[Dict]]" = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
"renamed": [],
|
||||
"not in cache": [],
|
||||
}
|
||||
|
||||
for change in idiff(
|
||||
old,
|
||||
new,
|
||||
with_renames=True,
|
||||
hash_only=True,
|
||||
):
|
||||
if change.typ == ADD:
|
||||
ret["added"].append(
|
||||
{
|
||||
"path": _path(change.new),
|
||||
"hash": _hash(change.new),
|
||||
}
|
||||
)
|
||||
elif change.typ == DELETE:
|
||||
ret["deleted"].append(
|
||||
{
|
||||
"path": _path(change.old),
|
||||
"hash": _hash(change.old),
|
||||
}
|
||||
)
|
||||
elif change.typ == MODIFY:
|
||||
ret["modified"].append(
|
||||
{
|
||||
"path": _path(change.old),
|
||||
"hash": {
|
||||
"old": _hash(change.old),
|
||||
"new": _hash(change.new),
|
||||
},
|
||||
}
|
||||
)
|
||||
elif change.typ == RENAME:
|
||||
ret["renamed"].append(
|
||||
{
|
||||
"path": {
|
||||
"old": _path(change.old),
|
||||
"new": _path(change.new),
|
||||
},
|
||||
"hash": _hash(change.old),
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
with_missing
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.storage_map.cache_exists(change.old)
|
||||
):
|
||||
ret["not in cache"].append(
|
||||
{
|
||||
"path": _path(change.old),
|
||||
"hash": _hash(change.old),
|
||||
}
|
||||
)
|
||||
|
||||
return ret if any(ret.values()) else {}
|
||||
|
||||
|
||||
@locked
|
||||
def diff(
|
||||
self,
|
||||
a_rev: str = "HEAD",
|
||||
b_rev: Optional[str] = None,
|
||||
targets: Optional[List[str]] = None,
|
||||
recursive: bool = False,
|
||||
):
|
||||
"""
|
||||
By default, it compares the workspace with the last commit's fs.
|
||||
@@ -42,231 +106,58 @@ def diff(
|
||||
the concept of `index`, but it keeps the same interface, thus,
|
||||
`dvc diff` would be the same as `dvc diff HEAD`.
|
||||
"""
|
||||
|
||||
if self.scm.no_commits:
|
||||
return {}
|
||||
|
||||
from dvc.fs.dvc import DVCFileSystem
|
||||
indexes = {}
|
||||
missing_targets = defaultdict(set)
|
||||
with_missing = False
|
||||
if not b_rev:
|
||||
b_rev = "workspace"
|
||||
with_missing = True
|
||||
|
||||
dvcfs = DVCFileSystem(repo=self)
|
||||
targets = [dvcfs.from_os_path(target) for target in ensure_list(targets)]
|
||||
|
||||
b_rev = b_rev if b_rev else "workspace"
|
||||
results = {}
|
||||
missing_targets = {}
|
||||
for rev in self.brancher(revs=[a_rev, b_rev]):
|
||||
if rev == "workspace" and rev != b_rev:
|
||||
if rev == "workspace" and b_rev != "workspace":
|
||||
# brancher always returns workspace, but we only need to compute
|
||||
# workspace paths/checksums if b_rev was None
|
||||
continue
|
||||
|
||||
targets_paths = None
|
||||
if targets:
|
||||
# convert targets to paths, and capture any missing targets
|
||||
targets_paths, missing_targets[rev] = _targets_to_paths(dvcfs, targets)
|
||||
def onerror(target, _exc):
|
||||
# pylint: disable-next=cell-var-from-loop
|
||||
missing_targets[rev].add(target) # noqa: B023
|
||||
|
||||
results[rev] = _paths_checksums(self, targets_paths)
|
||||
|
||||
if targets:
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
for target in set(missing_targets[a_rev]) & set(missing_targets[b_rev]):
|
||||
raise PathMissingError(target, self)
|
||||
|
||||
old = results[a_rev]
|
||||
new = results[b_rev]
|
||||
|
||||
# Compare paths between the old and new fs.
|
||||
# set() efficiently converts dict keys to a set
|
||||
added = sorted(set(new) - set(old))
|
||||
deleted_or_missing = set(old) - set(new)
|
||||
if b_rev == "workspace":
|
||||
# missing status is only applicable when diffing local workspace
|
||||
# against a commit
|
||||
missing = sorted(_filter_missing(dvcfs, deleted_or_missing))
|
||||
else:
|
||||
missing = []
|
||||
deleted = sorted(deleted_or_missing - set(missing))
|
||||
modified = sorted(set(old) & set(new))
|
||||
|
||||
# Cases when file was changed and renamed are resulted
|
||||
# in having deleted and added record
|
||||
# To cover such cases we need to change hashing function
|
||||
# to produce rolling/chunking hash
|
||||
|
||||
renamed = _calculate_renamed(new, old, added, deleted)
|
||||
|
||||
for renamed_item in renamed:
|
||||
added.remove(renamed_item["path"]["new"])
|
||||
deleted.remove(renamed_item["path"]["old"])
|
||||
|
||||
ret = {
|
||||
"added": [{"path": path, "hash": new[path]} for path in added],
|
||||
"deleted": [{"path": path, "hash": old[path]} for path in deleted],
|
||||
"modified": [
|
||||
{"path": path, "hash": {"old": old[path], "new": new[path]}}
|
||||
for path in modified
|
||||
if old[path] != new[path]
|
||||
],
|
||||
"renamed": renamed,
|
||||
"not in cache": [{"path": path, "hash": old[path]} for path in missing],
|
||||
}
|
||||
|
||||
return ret if any(ret.values()) else {}
|
||||
|
||||
|
||||
def _paths_checksums(
|
||||
repo: "Repo", targets: Optional[List[str]]
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
A dictionary of checksums addressed by relpaths collected from
|
||||
the current fs outputs.
|
||||
|
||||
To help distinguish between a directory and a file output,
|
||||
the former one will come with a trailing slash in the path:
|
||||
|
||||
directory: "data/"
|
||||
file: "data"
|
||||
"""
|
||||
|
||||
return dict(_output_paths(repo, targets))
|
||||
|
||||
|
||||
def _output_paths( # noqa: C901
|
||||
repo: "Repo", targets: Optional[List[str]]
|
||||
) -> Iterable[Tuple[str, Optional[str]]]:
|
||||
from dvc.fs import LocalFileSystem
|
||||
from dvc_data.hashfile.build import build
|
||||
|
||||
on_working_fs = isinstance(repo.fs, LocalFileSystem)
|
||||
|
||||
def _exists(output: "Output") -> bool:
|
||||
if on_working_fs:
|
||||
return output.exists
|
||||
return True
|
||||
|
||||
def _to_path(output: "Output") -> str:
|
||||
relparts = output.fs.path.relparts(output.fs_path)
|
||||
base = os.path.join(*relparts)
|
||||
if output.is_dir_checksum:
|
||||
return os.path.join(base, "")
|
||||
return base
|
||||
|
||||
for output in repo.index.outs:
|
||||
if not _exists(output):
|
||||
continue
|
||||
|
||||
yield_output = targets is None or any(
|
||||
output.fs.path.isin_or_eq(output.fs_path, target) for target in targets
|
||||
view = self.index.targets_view(
|
||||
targets,
|
||||
onerror=onerror,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
obj: Optional["HashFile"]
|
||||
if on_working_fs:
|
||||
_, _, obj = build(
|
||||
repo.odb.repo,
|
||||
output.fs_path,
|
||||
repo.odb.repo.fs,
|
||||
"md5",
|
||||
dry_run=True,
|
||||
ignore=output.dvcignore,
|
||||
)
|
||||
hash_info = obj.hash_info
|
||||
else:
|
||||
hash_info = output.hash_info
|
||||
obj = output.get_obj()
|
||||
if rev == "workspace":
|
||||
from .index import build_data_index
|
||||
|
||||
if yield_output:
|
||||
yield _to_path(output), hash_info.value
|
||||
|
||||
if not obj:
|
||||
continue
|
||||
|
||||
if output.is_dir_checksum and (
|
||||
yield_output
|
||||
or (
|
||||
isinstance(targets, list)
|
||||
and any(
|
||||
output.fs.path.isin(target, output.fs_path) for target in targets
|
||||
with ui.status("Building workspace index"):
|
||||
data = build_data_index(
|
||||
view,
|
||||
self.root_dir,
|
||||
self.fs,
|
||||
compute_hash=True,
|
||||
)
|
||||
)
|
||||
):
|
||||
obj = cast("Tree", obj)
|
||||
yield from _dir_output_paths(output.fs, output.fs_path, obj, targets)
|
||||
|
||||
|
||||
def _dir_output_paths(
|
||||
fs: "FileSystem",
|
||||
fs_path: str,
|
||||
obj: "Tree",
|
||||
targets: Optional[Iterable[str]] = None,
|
||||
) -> Iterator[Tuple[str, str]]:
|
||||
base = os.path.join(*fs.path.relparts(fs_path))
|
||||
for key, _, oid in obj:
|
||||
fname = fs.path.join(fs_path, *key)
|
||||
if targets is None or any(
|
||||
fs.path.isin_or_eq(fname, target) for target in targets
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
yield os.path.join(base, *key), oid.value
|
||||
|
||||
|
||||
def _filter_missing(dvcfs: "DVCFileSystem", paths: Set[str]) -> Iterator[str]:
|
||||
for path in paths:
|
||||
fs_path = dvcfs.from_os_path(path)
|
||||
try:
|
||||
info = dvcfs.info(fs_path)
|
||||
dvc_info = info.get("dvc_info", {})
|
||||
entry = dvc_info.get("entry")
|
||||
if (
|
||||
entry
|
||||
and info["type"] == "directory"
|
||||
and not dvcfs.repo.odb.local.exists(entry.hash_info.value)
|
||||
):
|
||||
yield path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def _targets_to_paths(
|
||||
dvcfs: "DVCFileSystem", targets: List[str]
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
paths: List[str] = []
|
||||
missing: List[str] = []
|
||||
|
||||
if not targets:
|
||||
return paths, missing
|
||||
|
||||
for target in targets:
|
||||
if dvcfs.exists(target):
|
||||
paths.append(dvcfs.repo.fs.path.abspath(target))
|
||||
else:
|
||||
missing.append(target)
|
||||
data = view.data["repo"]
|
||||
|
||||
return paths, missing
|
||||
assert rev not in indexes
|
||||
indexes[rev] = data
|
||||
|
||||
if targets:
|
||||
old_missing = missing_targets.get(a_rev, set())
|
||||
new_missing = missing_targets.get(b_rev, set())
|
||||
|
||||
def _calculate_renamed(
|
||||
new: Dict[str, Any],
|
||||
old: Dict[str, Any],
|
||||
added: Iterable[str],
|
||||
deleted: Iterable[str],
|
||||
) -> List[Dict[str, Dict[str, str]]]:
|
||||
old_inverted: Dict[str, List[str]] = defaultdict(list)
|
||||
# It is needed to be dict of lists to cover cases
|
||||
# when repo has paths with same hash
|
||||
for path, path_hash in old.items():
|
||||
old_inverted[path_hash].append(path)
|
||||
# check for overlapping missing targets between a_rev and b_rev
|
||||
for target in old_missing & new_missing:
|
||||
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), target)
|
||||
|
||||
renamed = []
|
||||
for path in added:
|
||||
path_hash = new[path]
|
||||
old_paths = old_inverted[path_hash]
|
||||
try:
|
||||
iterator = enumerate(old_paths)
|
||||
index = next(idx for idx, path in iterator if path in deleted)
|
||||
except StopIteration:
|
||||
continue
|
||||
old = indexes[a_rev]
|
||||
new = indexes[b_rev]
|
||||
|
||||
old_path = old_paths.pop(index)
|
||||
renamed.append({"path": {"old": old_path, "new": path}, "hash": path_hash})
|
||||
|
||||
return renamed
|
||||
with ui.status("Calculating diff"):
|
||||
return _diff(old, new, with_missing=with_missing)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional
|
||||
|
||||
from funcy import chain, first
|
||||
|
||||
@@ -11,17 +10,15 @@ from dvc.ui import ui
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
from .cache import ExpCache
|
||||
from .exceptions import (
|
||||
BaselineMismatchError,
|
||||
ExperimentExistsError,
|
||||
InvalidExpRefError,
|
||||
MultipleBranchError,
|
||||
)
|
||||
from .executor.base import BaseExecutor
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
from .refs import (
|
||||
APPLY_STASH,
|
||||
CELERY_FAILED_STASH,
|
||||
CELERY_STASH,
|
||||
EXEC_APPLY,
|
||||
@@ -31,10 +28,14 @@ from .refs import (
|
||||
WORKSPACE_STASH,
|
||||
ExpRefInfo,
|
||||
)
|
||||
from .stash import ApplyStash
|
||||
from .utils import check_ref_format, exp_refs_by_rev, unlocked_repo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .queue.base import BaseStashQueue, QueueEntry
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
from .queue.tempdir import TempDirQueue
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
from .stash import ExpStashEntry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -62,6 +63,11 @@ class Experiments:
|
||||
|
||||
@property
|
||||
def scm(self):
|
||||
from dvc.scm import SCMError
|
||||
|
||||
if self.repo.scm.no_commits:
|
||||
raise SCMError("Empty Git repo. Add a commit to use experiments.")
|
||||
|
||||
return self.repo.scm
|
||||
|
||||
@cached_property
|
||||
@@ -70,22 +76,38 @@ class Experiments:
|
||||
|
||||
@cached_property
|
||||
def args_file(self) -> str:
|
||||
from .executor.base import BaseExecutor
|
||||
|
||||
return os.path.join(self.repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
|
||||
@cached_property
|
||||
def workspace_queue(self) -> WorkspaceQueue:
|
||||
def workspace_queue(self) -> "WorkspaceQueue":
|
||||
from .queue.workspace import WorkspaceQueue
|
||||
|
||||
return WorkspaceQueue(self.repo, WORKSPACE_STASH)
|
||||
|
||||
@cached_property
|
||||
def tempdir_queue(self) -> TempDirQueue:
|
||||
def tempdir_queue(self) -> "TempDirQueue":
|
||||
from .queue.tempdir import TempDirQueue
|
||||
|
||||
# NOTE: tempdir and workspace stash is shared since both
|
||||
# implementations immediately push -> pop (queue length is only 0 or 1)
|
||||
return TempDirQueue(self.repo, WORKSPACE_STASH)
|
||||
|
||||
@cached_property
|
||||
def celery_queue(self) -> LocalCeleryQueue:
|
||||
def celery_queue(self) -> "LocalCeleryQueue":
|
||||
from .queue.celery import LocalCeleryQueue
|
||||
|
||||
return LocalCeleryQueue(self.repo, CELERY_STASH, CELERY_FAILED_STASH)
|
||||
|
||||
@cached_property
|
||||
def apply_stash(self) -> ApplyStash:
|
||||
return ApplyStash(self.scm, APPLY_STASH)
|
||||
|
||||
@cached_property
|
||||
def cache(self) -> ExpCache:
|
||||
return ExpCache(self.repo)
|
||||
|
||||
@property
|
||||
def stash_revs(self) -> Dict[str, "ExpStashEntry"]:
|
||||
revs = {}
|
||||
@@ -96,24 +118,18 @@ class Experiments:
|
||||
def reproduce_one(
|
||||
self,
|
||||
tmp_dir: bool = False,
|
||||
machine: Optional[str] = None,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
if not (tmp_dir or machine):
|
||||
staged, _, _ = self.scm.status(untracked_files="no")
|
||||
if staged:
|
||||
logger.warning(
|
||||
"Your workspace contains staged Git changes which will be "
|
||||
"unstaged before running this experiment."
|
||||
)
|
||||
self.scm.reset()
|
||||
|
||||
exp_queue: "BaseStashQueue" = (
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
self.queue_one(exp_queue, **kwargs)
|
||||
results = self._reproduce_queue(exp_queue)
|
||||
results = self._reproduce_queue(
|
||||
exp_queue, copy_paths=copy_paths, message=message
|
||||
)
|
||||
exp_rev = first(results)
|
||||
if exp_rev is not None:
|
||||
self._log_reproduced(results, tmp_dir=tmp_dir)
|
||||
@@ -192,8 +208,7 @@ class Experiments:
|
||||
)
|
||||
for entry in entries:
|
||||
# wait for task execution to start
|
||||
while not self.celery_queue.proc.get(entry.stash_rev):
|
||||
time.sleep(1)
|
||||
self.celery_queue.wait_for_start(entry, sleep_interval=1)
|
||||
self.celery_queue.follow(entry)
|
||||
# wait for task collection to complete
|
||||
try:
|
||||
@@ -336,7 +351,13 @@ class Experiments:
|
||||
self.scm.remove_ref(EXEC_APPLY)
|
||||
|
||||
@unlocked_repo
|
||||
def _reproduce_queue(self, queue: "BaseStashQueue", **kwargs) -> Dict[str, str]:
|
||||
def _reproduce_queue(
|
||||
self,
|
||||
queue: "BaseStashQueue",
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Dict[str, str]:
|
||||
"""Reproduce queued experiments.
|
||||
|
||||
Arguments:
|
||||
@@ -346,7 +367,7 @@ class Experiments:
|
||||
dict mapping successfully reproduced experiment revs to their
|
||||
results.
|
||||
"""
|
||||
exec_results = queue.reproduce()
|
||||
exec_results = queue.reproduce(copy_paths=copy_paths, message=message)
|
||||
|
||||
results: Dict[str, str] = {}
|
||||
for _, exp_result in exec_results.items():
|
||||
@@ -417,7 +438,7 @@ class Experiments:
|
||||
try:
|
||||
name = ExpRefInfo.from_ref(ref).name
|
||||
except InvalidExpRefError:
|
||||
pass # noqa: S110
|
||||
pass
|
||||
if not name:
|
||||
if rev in self.stash_revs:
|
||||
name = self.stash_revs[rev].name
|
||||
@@ -493,3 +514,8 @@ class Experiments:
|
||||
from dvc.repo.experiments.remove import remove
|
||||
|
||||
return remove(self.repo, *args, **kwargs)
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
from dvc.repo.experiments.clean import clean
|
||||
|
||||
return clean(self.repo, *args, **kwargs)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import Git
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
from .exceptions import ApplyConflictError, BaselineMismatchError, InvalidExpRevError
|
||||
from .exceptions import BaselineMismatchError, InvalidExpRevError
|
||||
from .executor.base import BaseExecutor
|
||||
from .refs import EXEC_APPLY
|
||||
|
||||
@@ -21,11 +21,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
|
||||
def apply(repo: "Repo", rev: str, **kwargs): # noqa: C901
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import GitMergeError, RevError, resolve_rev
|
||||
from dvc.scm import RevError, resolve_rev
|
||||
|
||||
exps: "Experiments" = repo.experiments
|
||||
|
||||
@@ -34,7 +32,6 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
assert isinstance(repo.scm, Git)
|
||||
try:
|
||||
exp_rev = resolve_rev(repo.scm, rev)
|
||||
exps.check_baseline(exp_rev)
|
||||
except RevError as exc:
|
||||
(
|
||||
exp_ref_info,
|
||||
@@ -42,14 +39,9 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
) = exps.celery_queue.get_ref_and_entry_by_names(
|
||||
rev
|
||||
)[rev]
|
||||
baseline_sha = repo.scm.get_rev()
|
||||
if exp_ref_info:
|
||||
if baseline_sha != exp_ref_info.baseline_sha:
|
||||
raise InvalidExpRevError(rev) # noqa: B904
|
||||
exp_rev = repo.scm.get_ref(str(exp_ref_info))
|
||||
elif queue_entry:
|
||||
if queue_entry.baseline_rev != baseline_sha:
|
||||
raise InvalidExpRevError(rev)
|
||||
exp_rev = queue_entry.stash_rev
|
||||
is_stash = True
|
||||
else:
|
||||
@@ -57,68 +49,22 @@ def apply(repo: "Repo", rev: str, force: bool = True, **kwargs): # noqa: C901
|
||||
except BaselineMismatchError as exc:
|
||||
raise InvalidExpRevError(rev) from exc
|
||||
|
||||
# NOTE: we don't use scmrepo's stash_workspace() here since we need
|
||||
# finer control over the merge behavior when we unstash everything
|
||||
with _apply_workspace(repo, rev, force):
|
||||
try:
|
||||
repo.scm.merge(exp_rev, commit=False, squash=True)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=repo.scm) # noqa: B904
|
||||
|
||||
repo.scm.reset()
|
||||
|
||||
if is_stash:
|
||||
assert repo.tmp_dir is not None
|
||||
args_path = os.path.join(repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
remove(args_path)
|
||||
|
||||
_apply(repo, exp_rev, name=rev, is_stash=is_stash)
|
||||
dvc_checkout(repo, **kwargs)
|
||||
|
||||
repo.scm.set_ref(EXEC_APPLY, exp_rev)
|
||||
logger.info(
|
||||
"Changes for experiment '%s' have been applied to your current workspace.",
|
||||
rev,
|
||||
ui.write(
|
||||
f"Changes for experiment '{rev}' have been applied to your current workspace.",
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _apply_workspace(repo: "Repo", rev: str, force: bool):
|
||||
from scmrepo.exceptions import MergeConflictError
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
def _apply(repo: "Repo", rev: str, name: Optional[str] = None, is_stash: bool = False):
|
||||
exps: "Experiments" = repo.experiments
|
||||
|
||||
assert isinstance(repo.scm, Git)
|
||||
if repo.scm.is_dirty(untracked_files=True):
|
||||
logger.debug("Stashing workspace")
|
||||
stash_rev: Optional[str] = repo.scm.stash.push(include_untracked=True)
|
||||
else:
|
||||
stash_rev = None
|
||||
try:
|
||||
yield
|
||||
except Exception: # pylint: disable=broad-except
|
||||
if stash_rev:
|
||||
_clean_and_pop(repo.scm)
|
||||
raise
|
||||
if not stash_rev:
|
||||
return
|
||||
|
||||
try:
|
||||
repo.scm.reset()
|
||||
repo.scm.stash.apply(stash_rev, skip_conflicts=force)
|
||||
repo.scm.stash.drop()
|
||||
except (MergeConflictError, _SCMError) as exc:
|
||||
_clean_and_pop(repo.scm)
|
||||
raise ApplyConflictError(rev) from exc
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_clean_and_pop(repo.scm)
|
||||
raise
|
||||
|
||||
|
||||
def _clean_and_pop(scm: "Git"):
|
||||
"""Revert any changes and pop the last stash entry."""
|
||||
scm.reset(hard=True)
|
||||
if scm.is_dirty(untracked_files=True):
|
||||
# drop any changes to untracked files before popping stash
|
||||
scm.stash.push(include_untracked=True)
|
||||
scm.stash.drop()
|
||||
scm.stash.pop()
|
||||
with exps.apply_stash.preserve_workspace(rev, name=name):
|
||||
with repo.scm.detach_head(rev, force=True):
|
||||
if is_stash:
|
||||
assert repo.tmp_dir is not None
|
||||
args_path = os.path.join(repo.tmp_dir, BaseExecutor.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
remove(args_path)
|
||||
|
||||
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def branch(repo, exp_rev, branch_name, *args, **kwargs):
|
||||
def branch(repo, exp_rev, branch_name=None, **kwargs):
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
try:
|
||||
@@ -46,6 +46,8 @@ def branch(repo, exp_rev, branch_name, *args, **kwargs):
|
||||
if not ref_info:
|
||||
raise InvalidExpRevError(exp_rev)
|
||||
|
||||
branch_name = branch_name or f"{ref_info.name}-branch"
|
||||
|
||||
branch_ref = f"refs/heads/{branch_name}"
|
||||
if repo.scm.get_ref(branch_ref):
|
||||
raise InvalidArgumentError(f"Git branch '{branch_name}' already exists.")
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from dvc.fs import localfs
|
||||
from dvc_objects.db import ObjectDB
|
||||
|
||||
from .serialize import DeserializeError, SerializableError, SerializableExp
|
||||
from .utils import EXEC_TMP_DIR
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExpCache:
|
||||
"""Serialized experiment state cache.
|
||||
|
||||
ODB with git SHAs as keys. Objects can be either SerializableExp or
|
||||
SerializableError.
|
||||
"""
|
||||
|
||||
CACHE_DIR = os.path.join(EXEC_TMP_DIR, "cache")
|
||||
|
||||
def __init__(self, repo: "Repo"):
|
||||
path = os.path.join(repo.tmp_dir, self.CACHE_DIR)
|
||||
self.odb = ObjectDB(localfs, path)
|
||||
|
||||
def delete(self, rev: str):
|
||||
self.odb.delete(rev)
|
||||
|
||||
def put(
|
||||
self,
|
||||
exp: Union[SerializableExp, SerializableError],
|
||||
rev: Optional[str] = None,
|
||||
force: bool = False,
|
||||
):
|
||||
rev = rev or getattr(exp, "rev", None)
|
||||
assert rev
|
||||
assert rev != "workspace"
|
||||
if force or not self.odb.exists(rev):
|
||||
try:
|
||||
self.delete(rev)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.odb.add_bytes(rev, exp.as_bytes())
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"ExpCache: cache put '%s'", rev[:7]
|
||||
)
|
||||
|
||||
def get(self, rev: str) -> Optional[Union[SerializableExp, SerializableError]]:
|
||||
obj = self.odb.get(rev)
|
||||
try:
|
||||
with obj.fs.open(obj.path, "rb") as fobj:
|
||||
data = fobj.read()
|
||||
except FileNotFoundError:
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"ExpCache: cache miss '%s'", rev[:7]
|
||||
)
|
||||
return None
|
||||
for typ in (SerializableExp, SerializableError):
|
||||
try:
|
||||
exp = typ.from_bytes(data) # type: ignore[attr-defined]
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"ExpCache: cache load '%s'", rev[:7]
|
||||
)
|
||||
return exp
|
||||
except DeserializeError:
|
||||
continue
|
||||
logger.debug("ExpCache: unknown object type for '%s'", rev)
|
||||
return None
|
||||
@@ -0,0 +1,12 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
def clean(repo: "Repo"):
|
||||
ui.write("Cleaning up dvc-task messages...")
|
||||
repo.experiments.celery_queue.celery.clean()
|
||||
ui.write("Done!")
|
||||
@@ -0,0 +1,380 @@
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Collection,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import first
|
||||
from scmrepo.exceptions import SCMError as InnerSCMError
|
||||
|
||||
from dvc.scm import Git, SCMError, iter_revs
|
||||
|
||||
from .exceptions import InvalidExpRefError
|
||||
from .refs import EXEC_BRANCH, ExpRefInfo
|
||||
from .serialize import ExpRange, ExpState, SerializableError, SerializableExp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
from .cache import ExpCache
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def collect_rev(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
param_deps: bool = False,
|
||||
force: bool = False,
|
||||
cache: Optional["ExpCache"] = None,
|
||||
**kwargs,
|
||||
) -> ExpState:
|
||||
"""Collect experiment state for the given revision.
|
||||
|
||||
Exp will be loaded from cache when available unless rev is 'workspace' or
|
||||
force is set.
|
||||
"""
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
cache = cache or repo.experiments.cache
|
||||
assert cache
|
||||
# TODO: support filtering serialized exp when param_deps is set
|
||||
if rev != "workspace" and not (force or param_deps):
|
||||
cached_exp = cache.get(rev)
|
||||
if cached_exp:
|
||||
if isinstance(cached_exp, SerializableError):
|
||||
return ExpState(rev=rev, error=cached_exp)
|
||||
return ExpState(rev=rev, data=cached_exp)
|
||||
if rev == "workspace" and isinstance(repo.fs, LocalFileSystem):
|
||||
orig_cwd: Optional[str] = os.getcwd()
|
||||
os.chdir(repo.root_dir)
|
||||
else:
|
||||
orig_cwd = None
|
||||
try:
|
||||
data = _collect_rev(
|
||||
repo,
|
||||
rev,
|
||||
param_deps=param_deps,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
if not (rev == "workspace" or param_deps or data.contains_error):
|
||||
cache.put(data, force=True)
|
||||
return ExpState(rev=rev, data=data)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
logger.debug("", exc_info=True)
|
||||
error = SerializableError(str(exc), type(exc).__name__)
|
||||
return ExpState(rev=rev, error=error)
|
||||
finally:
|
||||
if orig_cwd:
|
||||
os.chdir(orig_cwd)
|
||||
|
||||
|
||||
def _collect_rev(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
param_deps: bool = False,
|
||||
**kwargs,
|
||||
) -> SerializableExp:
|
||||
with repo.switch(rev) as rev:
|
||||
if rev == "workspace":
|
||||
timestamp: Optional[datetime] = None
|
||||
else:
|
||||
commit = repo.scm.resolve_commit(rev)
|
||||
timestamp = datetime.fromtimestamp(commit.commit_time)
|
||||
|
||||
return SerializableExp.from_repo(
|
||||
repo,
|
||||
rev=rev,
|
||||
param_deps=param_deps,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
|
||||
|
||||
def collect_branch(
|
||||
repo: "Repo",
|
||||
rev: str,
|
||||
end_rev: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Iterator["ExpState"]:
|
||||
"""Iterate over exp states in a Git branch.
|
||||
|
||||
Git branch will be traversed in reverse, starting from rev.
|
||||
|
||||
Args:
|
||||
rev: Branch tip (head).
|
||||
end_rev: If specified, traversal will stop when end_rev is reached
|
||||
(exclusive, end_rev will not be collected).
|
||||
"""
|
||||
try:
|
||||
for branch_rev in repo.scm.branch_revs(rev, end_rev):
|
||||
yield collect_rev(repo, branch_rev, **kwargs)
|
||||
except (SCMError, InnerSCMError):
|
||||
pass
|
||||
|
||||
|
||||
def collect_exec_branch(
|
||||
repo: "Repo",
|
||||
baseline_rev: str,
|
||||
**kwargs,
|
||||
) -> Iterator["ExpState"]:
|
||||
"""Iterate over active experiment branch for the current executor."""
|
||||
last_rev = repo.scm.get_ref(EXEC_BRANCH) or repo.scm.get_rev()
|
||||
last_rev = repo.scm.get_rev()
|
||||
yield collect_rev(repo, "workspace", **kwargs)
|
||||
if last_rev != baseline_rev:
|
||||
yield from collect_branch(repo, last_rev, baseline_rev, **kwargs)
|
||||
|
||||
|
||||
def collect_queued(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect queued experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of queued experiments.
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
return repo.experiments.celery_queue.collect_queued_data(baseline_revs, **kwargs)
|
||||
|
||||
|
||||
def collect_active(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect active (running) experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of active experiments.
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
result: Dict[str, List["ExpRange"]] = {}
|
||||
exps = repo.experiments
|
||||
for queue in (exps.workspace_queue, exps.tempdir_queue, exps.celery_queue):
|
||||
for baseline, active_exps in queue.collect_active_data(
|
||||
baseline_revs, **kwargs
|
||||
).items():
|
||||
if baseline in result:
|
||||
result[baseline].extend(active_exps)
|
||||
else:
|
||||
result[baseline] = list(active_exps)
|
||||
return result
|
||||
|
||||
|
||||
def collect_failed(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect failed experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of active experiments.
|
||||
"""
|
||||
if not baseline_revs:
|
||||
return {}
|
||||
return repo.experiments.celery_queue.collect_failed_data(baseline_revs, **kwargs)
|
||||
|
||||
|
||||
def collect_successful(
|
||||
repo: "Repo",
|
||||
baseline_revs: Collection[str],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect successful experiments derived from the specified revisions.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to successful experiments.
|
||||
"""
|
||||
result: Dict[str, List["ExpRange"]] = {}
|
||||
for baseline_rev in baseline_revs:
|
||||
result[baseline_rev] = list(_collect_baseline(repo, baseline_rev, **kwargs))
|
||||
return result
|
||||
|
||||
|
||||
def _collect_baseline(
|
||||
repo: "Repo",
|
||||
baseline_rev: str,
|
||||
**kwargs,
|
||||
) -> Iterator["ExpRange"]:
|
||||
"""Iterate over experiments derived from a baseline revision.
|
||||
|
||||
Args:
|
||||
repo: Repo.
|
||||
baseline_revs: Resolved baseline Git SHAs.
|
||||
|
||||
Yields:
|
||||
Tuple of (timestamp, exp_range).
|
||||
"""
|
||||
ref_info = ExpRefInfo(baseline_sha=baseline_rev)
|
||||
refs: Optional[Iterable[str]] = kwargs.get("refs")
|
||||
if refs:
|
||||
ref_it = (ref for ref in iter(refs) if ref.startswith(str(ref_info)))
|
||||
else:
|
||||
ref_it = repo.scm.iter_refs(base=str(ref_info))
|
||||
for ref in ref_it:
|
||||
try:
|
||||
ref_info = ExpRefInfo.from_ref(ref)
|
||||
exp_rev = repo.scm.get_ref(ref)
|
||||
if not exp_rev:
|
||||
continue
|
||||
except (InvalidExpRefError, SCMError, InnerSCMError):
|
||||
continue
|
||||
exps = list(collect_branch(repo, exp_rev, baseline_rev, **kwargs))
|
||||
if exps:
|
||||
exps[0].name = ref_info.name
|
||||
yield ExpRange(exps, name=ref_info.name)
|
||||
|
||||
|
||||
def collect(
|
||||
repo: "Repo",
|
||||
revs: Union[List[str], str, None] = None,
|
||||
all_branches: bool = False,
|
||||
all_tags: bool = False,
|
||||
all_commits: bool = False,
|
||||
num: int = 1,
|
||||
hide_queued: bool = False,
|
||||
hide_failed: bool = False,
|
||||
sha_only: bool = False,
|
||||
**kwargs,
|
||||
) -> List["ExpState"]:
|
||||
"""Collect baseline revisions and derived experiments."""
|
||||
assert isinstance(repo.scm, Git)
|
||||
if repo.scm.no_commits:
|
||||
return []
|
||||
if not any([revs, all_branches, all_tags, all_commits]):
|
||||
revs = ["HEAD"]
|
||||
if isinstance(revs, str):
|
||||
revs = [revs]
|
||||
cached_refs = list(repo.scm.iter_refs())
|
||||
baseline_revs = list(
|
||||
iter_revs(
|
||||
repo.scm,
|
||||
revs=revs,
|
||||
num=num,
|
||||
all_branches=all_branches,
|
||||
all_tags=all_tags,
|
||||
all_commits=all_commits,
|
||||
)
|
||||
)
|
||||
if sha_only:
|
||||
baseline_names: Dict[str, Optional[str]] = {}
|
||||
else:
|
||||
baseline_names = _describe(repo.scm, baseline_revs, refs=cached_refs)
|
||||
|
||||
workspace_data = collect_rev(repo, "workspace", **kwargs)
|
||||
result: List["ExpState"] = [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 {}
|
||||
successful = collect_successful(repo, baseline_revs, **kwargs)
|
||||
|
||||
for baseline_rev in baseline_revs:
|
||||
baseline_data = collect_rev(repo, baseline_rev)
|
||||
experiments = list(
|
||||
itertools.chain.from_iterable(
|
||||
_sorted_ranges(collected.get(baseline_rev, []))
|
||||
for collected in (active, successful, queued, failed)
|
||||
)
|
||||
)
|
||||
result.append(
|
||||
ExpState(
|
||||
rev=baseline_rev,
|
||||
name=baseline_names.get(baseline_rev),
|
||||
data=baseline_data.data,
|
||||
error=baseline_data.error,
|
||||
experiments=experiments if experiments else None,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _describe(
|
||||
scm: "Git",
|
||||
revs: Iterable[str],
|
||||
refs: Optional[Iterable[str]] = None,
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""Describe revisions using a tag, branch.
|
||||
|
||||
The first matching name will be returned for each rev. Names are preferred in this
|
||||
order:
|
||||
- current branch (if rev matches HEAD and HEAD is a branch)
|
||||
- tags
|
||||
- branches
|
||||
|
||||
Returns:
|
||||
Dict mapping revisions from revs to a name.
|
||||
"""
|
||||
|
||||
head_rev = scm.get_rev()
|
||||
head_ref = scm.get_ref("HEAD", follow=False)
|
||||
if head_ref and head_ref.startswith("refs/heads/"):
|
||||
head_branch = head_ref[len("refs/heads/") :]
|
||||
else:
|
||||
head_branch = None
|
||||
|
||||
tags = {}
|
||||
branches = {}
|
||||
ref_it = iter(refs) if refs else scm.iter_refs()
|
||||
for ref in ref_it:
|
||||
is_tag = ref.startswith("refs/tags/")
|
||||
is_branch = ref.startswith("refs/heads/")
|
||||
if not (is_tag or is_branch):
|
||||
continue
|
||||
rev = scm.get_ref(ref)
|
||||
if not rev:
|
||||
logger.debug("unresolved ref %s", ref)
|
||||
continue
|
||||
if is_tag and rev not in tags:
|
||||
tags[rev] = ref[len("refs/tags/") :]
|
||||
if is_branch and rev not in branches:
|
||||
branches[rev] = ref[len("refs/heads/") :]
|
||||
names: Dict[str, Optional[str]] = {}
|
||||
for rev in revs:
|
||||
if rev == head_rev and head_branch:
|
||||
names[rev] = head_branch
|
||||
else:
|
||||
names[rev] = tags.get(rev) or branches.get(rev)
|
||||
return names
|
||||
|
||||
|
||||
def _sorted_ranges(exp_ranges: Iterable["ExpRange"]) -> List["ExpRange"]:
|
||||
"""Return list of ExpRange sorted by timestamp."""
|
||||
|
||||
def _head_timestamp(exp_range: "ExpRange") -> datetime:
|
||||
head_exp = first(exp_range.revs)
|
||||
if head_exp and head_exp.data and head_exp.data.timestamp:
|
||||
return head_exp.data.timestamp
|
||||
return datetime.fromtimestamp(0)
|
||||
|
||||
return sorted(exp_ranges, key=_head_timestamp, reverse=True)
|
||||
@@ -7,7 +7,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
from dvc.repo.experiments.show import collect_experiment_commit
|
||||
from dvc.repo.experiments.collect import collect_rev
|
||||
from dvc.scm import resolve_rev
|
||||
|
||||
if repo.scm.no_commits:
|
||||
@@ -15,22 +15,21 @@ def diff(repo, *args, a_rev=None, b_rev=None, param_deps=False, **kwargs):
|
||||
|
||||
if a_rev:
|
||||
rev = resolve_rev(repo.scm, a_rev)
|
||||
old = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
old = collect_experiment_commit(repo, "HEAD", param_deps=param_deps)
|
||||
rev = resolve_rev(repo.scm, "HEAD")
|
||||
old = collect_rev(repo, rev, param_deps=param_deps)
|
||||
|
||||
if b_rev:
|
||||
rev = resolve_rev(repo.scm, b_rev)
|
||||
new = collect_experiment_commit(repo, rev, param_deps=param_deps)
|
||||
else:
|
||||
new = collect_experiment_commit(repo, "workspace", param_deps=param_deps)
|
||||
rev = "workspace"
|
||||
new = collect_rev(repo, rev, param_deps=param_deps)
|
||||
|
||||
with_unchanged = kwargs.pop("all", False)
|
||||
|
||||
return {
|
||||
key: _diff(
|
||||
format_dict(old.get("data", {}).get(key, {})),
|
||||
format_dict(new.get("data", {}).get(key, {})),
|
||||
format_dict(getattr(old.data, key, {})),
|
||||
format_dict(getattr(new.data, key, {})),
|
||||
with_unchanged=with_unchanged,
|
||||
)
|
||||
for key in ["metrics", "params"]
|
||||
|
||||
@@ -66,18 +66,6 @@ class MultipleBranchError(DvcException):
|
||||
self.ref_infos = ref_infos
|
||||
|
||||
|
||||
class ApplyConflictError(InvalidArgumentError):
|
||||
def __init__(self, name):
|
||||
super().__init__(
|
||||
f"Experiment '{name}' cannot be applied to because your current "
|
||||
"workspace contains changes which would be overwritten. Either "
|
||||
"'git stash' your current changes before applying this "
|
||||
"experiment, or re-run this command with '--force' to overwrite "
|
||||
"your current changes."
|
||||
)
|
||||
self.name = name
|
||||
|
||||
|
||||
class AmbiguousExpRefInfo(InvalidArgumentError):
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import pickle # nosec B403
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import IntEnum
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
@@ -21,6 +24,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import get_in
|
||||
from scmrepo.exceptions import SCMError
|
||||
|
||||
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
|
||||
@@ -32,10 +36,14 @@ from dvc.repo.experiments.refs import (
|
||||
EXEC_CHECKPOINT,
|
||||
ExpRefInfo,
|
||||
)
|
||||
from dvc.repo.experiments.utils import to_studio_params
|
||||
from dvc.repo.metrics.show import _collect_top_level_metrics
|
||||
from dvc.repo.params.show import _collect_top_level_params
|
||||
from dvc.stage.serialize import to_lockfile
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import dict_sha256, env2bool, relpath
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.studio import env_to_config
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from queue import Queue
|
||||
@@ -254,6 +262,80 @@ class BaseExecutor(ABC):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _get_top_level_paths(cls, repo: "Repo") -> List["str"]:
|
||||
return list(
|
||||
chain(
|
||||
_collect_top_level_metrics(repo),
|
||||
_collect_top_level_params(repo),
|
||||
repo.index._plot_sources, # pylint: disable=protected-access
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def save(
|
||||
cls,
|
||||
info: "ExecutorInfo",
|
||||
force: bool = False,
|
||||
include_untracked: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
) -> ExecutorResult:
|
||||
from dvc.dvcfile import LOCK_FILE
|
||||
from dvc.repo import Repo
|
||||
|
||||
exp_hash: Optional[str] = None
|
||||
exp_ref: Optional[ExpRefInfo] = None
|
||||
|
||||
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
|
||||
old_cwd = os.getcwd()
|
||||
if info.wdir:
|
||||
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
|
||||
else:
|
||||
os.chdir(dvc.root_dir)
|
||||
|
||||
include_untracked = include_untracked or []
|
||||
include_untracked.extend(cls._get_top_level_paths(dvc))
|
||||
# dvc repro automatically stages dvc.lock. Running redundant `git add`
|
||||
# on it causes an error when exiting the detached head context.
|
||||
if LOCK_FILE in dvc.scm.untracked_files():
|
||||
include_untracked.append(LOCK_FILE)
|
||||
|
||||
try:
|
||||
stages = dvc.commit([], force=True, relink=False)
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if include_untracked:
|
||||
dvc.scm.add(include_untracked)
|
||||
cls.commit(
|
||||
dvc.scm, # type: ignore[arg-type]
|
||||
exp_hash,
|
||||
exp_name=info.name,
|
||||
force=force,
|
||||
message=message,
|
||||
)
|
||||
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
|
||||
exp_ref = ExpRefInfo.from_ref(ref) if ref else None
|
||||
untracked = dvc.scm.untracked_files()
|
||||
if untracked:
|
||||
logger.warning(
|
||||
"The following untracked files were present in "
|
||||
"the workspace before saving but "
|
||||
"will not be included in the experiment commit:\n"
|
||||
"\t%s",
|
||||
", ".join(untracked),
|
||||
)
|
||||
info.result_hash = exp_hash
|
||||
info.result_ref = ref
|
||||
info.result_force = False
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except DvcException:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
|
||||
return ExecutorResult(ref, exp_ref, info.result_force)
|
||||
|
||||
@staticmethod
|
||||
def hash_exp(stages: Iterable["PipelineStage"]) -> str:
|
||||
from dvc.stage import PipelineStage
|
||||
@@ -264,7 +346,7 @@ class BaseExecutor(ABC):
|
||||
exp_data.update(to_lockfile(stage))
|
||||
return dict_sha256(exp_data)
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
if infofile is not None:
|
||||
info = ExecutorInfo.load_json(infofile)
|
||||
if info.status < TaskStatus.FAILED:
|
||||
@@ -291,7 +373,7 @@ class BaseExecutor(ABC):
|
||||
@staticmethod
|
||||
def unpack_repro_args(path):
|
||||
with open(path, "rb") as fobj:
|
||||
data = pickle.load(fobj)
|
||||
data = pickle.load(fobj) # noqa: S301 # nosec B301
|
||||
return data["args"], data["kwargs"]
|
||||
|
||||
def fetch_exps(
|
||||
@@ -334,21 +416,17 @@ class BaseExecutor(ABC):
|
||||
|
||||
# fetch experiments
|
||||
try:
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{ref}:{ref}" for ref in refs],
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force,
|
||||
**kwargs,
|
||||
)
|
||||
refspecs = [f"{ref}:{ref}" for ref in refs]
|
||||
# update last run checkpoint (if it exists)
|
||||
if has_checkpoint:
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
[f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}"],
|
||||
force=True,
|
||||
**kwargs,
|
||||
)
|
||||
refspecs.append(f"{EXEC_CHECKPOINT}:{EXEC_CHECKPOINT}")
|
||||
dest_scm.fetch_refspecs(
|
||||
self.git_url,
|
||||
refspecs,
|
||||
on_diverged=on_diverged_ref,
|
||||
force=force or has_checkpoint,
|
||||
**kwargs,
|
||||
)
|
||||
except SCMError:
|
||||
pass
|
||||
|
||||
@@ -385,6 +463,8 @@ class BaseExecutor(ABC):
|
||||
infofile: Optional[str] = None,
|
||||
log_errors: bool = True,
|
||||
log_level: Optional[int] = None,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> "ExecutorResult":
|
||||
"""Run dvc repro and return the result.
|
||||
@@ -421,6 +501,8 @@ class BaseExecutor(ABC):
|
||||
info,
|
||||
infofile,
|
||||
log_errors=log_errors,
|
||||
copy_paths=copy_paths,
|
||||
message=message,
|
||||
**kwargs,
|
||||
) as dvc:
|
||||
if auto_push:
|
||||
@@ -471,6 +553,7 @@ class BaseExecutor(ABC):
|
||||
info.name,
|
||||
repro_force or checkpoint_reset,
|
||||
)
|
||||
|
||||
stages = dvc_reproduce(
|
||||
dvc,
|
||||
*args,
|
||||
@@ -478,6 +561,9 @@ class BaseExecutor(ABC):
|
||||
checkpoint_func=checkpoint_func,
|
||||
**kwargs,
|
||||
)
|
||||
if paths := cls._get_top_level_paths(dvc):
|
||||
logger.debug("Staging top-level files: %s", paths)
|
||||
dvc.scm_context.add(paths)
|
||||
|
||||
exp_hash = cls.hash_exp(stages)
|
||||
if not repro_dry:
|
||||
@@ -489,6 +575,7 @@ class BaseExecutor(ABC):
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message=message,
|
||||
)
|
||||
info.result_hash = exp_hash
|
||||
info.result_ref = ref
|
||||
@@ -509,6 +596,7 @@ class BaseExecutor(ABC):
|
||||
auto_push,
|
||||
git_remote,
|
||||
repro_force,
|
||||
message: Optional[str] = None,
|
||||
) -> Tuple[Optional[str], Optional["ExpRefInfo"], bool]:
|
||||
is_checkpoint = any(stage.is_checkpoint for stage in stages)
|
||||
cls.commit(
|
||||
@@ -517,6 +605,7 @@ class BaseExecutor(ABC):
|
||||
exp_name=info.name,
|
||||
force=repro_force,
|
||||
checkpoint=is_checkpoint,
|
||||
message=message,
|
||||
)
|
||||
if auto_push:
|
||||
cls._auto_push(dvc, dvc.scm, git_remote)
|
||||
@@ -543,45 +632,79 @@ class BaseExecutor(ABC):
|
||||
info: "ExecutorInfo",
|
||||
infofile: Optional[str] = None,
|
||||
log_errors: bool = True,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
) -> Iterator["Repo"]:
|
||||
from dvc_studio_client.post_live_metrics import post_live_metrics
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.stage.monitor import CheckpointKilledError
|
||||
|
||||
dvc = Repo(os.path.join(info.root_dir, info.dvc_dir))
|
||||
info.status = TaskStatus.RUNNING
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
if cls.QUIET:
|
||||
dvc.scm_context.quiet = cls.QUIET
|
||||
old_cwd = os.getcwd()
|
||||
if info.wdir:
|
||||
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
|
||||
else:
|
||||
os.chdir(dvc.root_dir)
|
||||
|
||||
try:
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
with Repo(os.path.join(info.root_dir, info.dvc_dir)) as dvc:
|
||||
info.status = TaskStatus.RUNNING
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
dvc.close()
|
||||
os.chdir(old_cwd)
|
||||
if cls.QUIET:
|
||||
dvc.scm_context.quiet = cls.QUIET
|
||||
old_cwd = os.getcwd()
|
||||
|
||||
for path in copy_paths or []:
|
||||
cls._copy_path(os.path.realpath(path), os.path.join(dvc.root_dir, path))
|
||||
|
||||
if info.wdir:
|
||||
os.chdir(os.path.join(dvc.scm.root_dir, info.wdir))
|
||||
else:
|
||||
os.chdir(dvc.root_dir)
|
||||
|
||||
args_path = os.path.join(dvc.tmp_dir, cls.PACKED_ARGS_FILE)
|
||||
if os.path.exists(args_path):
|
||||
_, kwargs = cls.unpack_repro_args(args_path)
|
||||
dvc_studio_config = dvc.config.get("studio")
|
||||
# set missing config options using saved config
|
||||
# inferring repo url will fail if not set here
|
||||
run_env_config = env_to_config(kwargs.get("run_env", {}))
|
||||
dvc_studio_config = {**run_env_config, **dvc_studio_config}
|
||||
try:
|
||||
post_live_metrics(
|
||||
"start",
|
||||
info.baseline_rev,
|
||||
info.name, # type: ignore[arg-type]
|
||||
"dvc",
|
||||
params=to_studio_params(dvc.params.show()),
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
message=message,
|
||||
)
|
||||
logger.debug("Running repro in '%s'", os.getcwd())
|
||||
yield dvc
|
||||
info.status = TaskStatus.SUCCESS
|
||||
except CheckpointKilledError:
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except DvcException:
|
||||
if log_errors:
|
||||
logger.exception("")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
except Exception:
|
||||
if log_errors:
|
||||
logger.exception("unexpected error")
|
||||
info.status = TaskStatus.FAILED
|
||||
raise
|
||||
finally:
|
||||
post_live_metrics(
|
||||
"done",
|
||||
info.baseline_rev,
|
||||
info.name, # type: ignore[arg-type]
|
||||
"dvc",
|
||||
experiment_rev=dvc.experiments.scm.get_ref(EXEC_BRANCH),
|
||||
metrics=get_in(dvc.metrics.show(), ["", "data"]),
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
)
|
||||
|
||||
if infofile is not None:
|
||||
info.dump_json(infofile)
|
||||
os.chdir(old_cwd)
|
||||
|
||||
@classmethod
|
||||
def _repro_args(cls, dvc):
|
||||
@@ -590,7 +713,7 @@ class BaseExecutor(ABC):
|
||||
args, kwargs = cls.unpack_repro_args(args_path)
|
||||
remove(args_path)
|
||||
# explicitly git rm/unstage the args file
|
||||
dvc.scm.add([args_path])
|
||||
dvc.scm.add([args_path], force=True)
|
||||
else:
|
||||
args = []
|
||||
kwargs = {}
|
||||
@@ -648,6 +771,7 @@ class BaseExecutor(ABC):
|
||||
exp_name: Optional[str] = None,
|
||||
force: bool = False,
|
||||
checkpoint: bool = False,
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
"""Commit stages as an experiment and return the commit SHA."""
|
||||
rev = scm.get_rev()
|
||||
@@ -677,7 +801,8 @@ class BaseExecutor(ABC):
|
||||
logger.debug("Commit to new experiment branch '%s'", branch)
|
||||
|
||||
scm.add([], update=True)
|
||||
scm.commit(f"dvc: commit experiment {exp_hash}", no_verify=True)
|
||||
message = message or f"dvc: commit experiment {exp_hash}"
|
||||
scm.commit(message, no_verify=True)
|
||||
new_rev = scm.get_rev()
|
||||
if check_conflict:
|
||||
new_rev = cls._raise_ref_conflict(scm, branch, new_rev, checkpoint)
|
||||
@@ -702,16 +827,27 @@ class BaseExecutor(ABC):
|
||||
|
||||
@staticmethod
|
||||
def _set_log_level(level):
|
||||
from dvc.logger import disable_other_loggers
|
||||
|
||||
# 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")
|
||||
disable_other_loggers()
|
||||
if level is not None:
|
||||
dvc_logger.setLevel(level)
|
||||
|
||||
@staticmethod
|
||||
def _copy_path(src, dst):
|
||||
try:
|
||||
if os.path.isfile(src):
|
||||
shutil.copy(src, dst)
|
||||
elif os.path.isdir(src):
|
||||
shutil.copytree(src, dst)
|
||||
else:
|
||||
raise DvcException(
|
||||
f"Unable to copy '{src}'. It is not a file or directory."
|
||||
)
|
||||
except OSError as exc:
|
||||
raise DvcException(f"Unable to copy '{src}' to '{dst}'.") from exc
|
||||
|
||||
@contextmanager
|
||||
def set_temp_refs(self, scm: "Git", temp_dict: Dict[str, str]):
|
||||
try:
|
||||
|
||||
@@ -4,8 +4,8 @@ from contextlib import ExitStack
|
||||
from tempfile import mkdtemp
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
|
||||
from configobj import ConfigObj
|
||||
from funcy import retry
|
||||
from scmrepo.exceptions import SCMError as _SCMError
|
||||
from shortuuid import uuid
|
||||
|
||||
from dvc.lock import LockError
|
||||
@@ -17,10 +17,10 @@ from dvc.repo.experiments.refs import (
|
||||
EXEC_HEAD,
|
||||
EXEC_MERGE,
|
||||
EXEC_NAMESPACE,
|
||||
EXPS_TEMP,
|
||||
TEMP_NAMESPACE,
|
||||
)
|
||||
from dvc.repo.experiments.utils import EXEC_TMP_DIR, get_exp_rwlock
|
||||
from dvc.scm import SCM, Git, GitMergeError
|
||||
from dvc.scm import SCM, Git
|
||||
from dvc.utils.fs import remove
|
||||
from dvc.utils.objects import cached_property
|
||||
|
||||
@@ -49,7 +49,7 @@ class BaseLocalExecutor(BaseExecutor):
|
||||
def scm(self) -> Union["Git", "NoSCM"]:
|
||||
return SCM(self.root_dir)
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
self.scm.close()
|
||||
del self.scm
|
||||
super().cleanup(infofile)
|
||||
@@ -90,9 +90,9 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
if infofile:
|
||||
self.info.dump_json(infofile)
|
||||
|
||||
temp_head = f"{EXPS_TEMP}/head-{uuid()}"
|
||||
temp_merge = f"{EXPS_TEMP}/merge-{uuid()}"
|
||||
temp_baseline = f"{EXPS_TEMP}/baseline-{uuid()}"
|
||||
temp_head = f"{TEMP_NAMESPACE}/head-{uuid()}"
|
||||
temp_merge = f"{TEMP_NAMESPACE}/merge-{uuid()}"
|
||||
temp_baseline = f"{TEMP_NAMESPACE}/baseline-{uuid()}"
|
||||
|
||||
temp_ref_dict = {
|
||||
temp_head: entry.head_rev,
|
||||
@@ -134,28 +134,32 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
self.scm.checkout(head, detach=True)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=self.scm) # noqa: B904
|
||||
self.scm.stash.apply(merge_rev)
|
||||
self._update_config(repo.config.read("local"))
|
||||
|
||||
def _config(self, cache_dir):
|
||||
def _update_config(self, update):
|
||||
local_config = os.path.join(
|
||||
self.root_dir,
|
||||
self.dvc_dir,
|
||||
"config.local",
|
||||
)
|
||||
logger.debug("Writing experiments local config '%s'", local_config)
|
||||
with open(local_config, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(f"[cache]\n dir = {cache_dir}")
|
||||
if os.path.exists(local_config):
|
||||
conf_obj = ConfigObj(local_config)
|
||||
conf_obj.merge(update)
|
||||
else:
|
||||
conf_obj = ConfigObj(update)
|
||||
if conf_obj:
|
||||
with open(local_config, "wb") as fobj:
|
||||
conf_obj.write(fobj)
|
||||
|
||||
def init_cache(
|
||||
self, repo: "Repo", rev: str, run_cache: bool = True # noqa: ARG002
|
||||
):
|
||||
"""Initialize DVC cache."""
|
||||
self._config(repo.odb.repo.path)
|
||||
self._update_config({"cache": {"dir": repo.cache.repo.path}})
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
super().cleanup(infofile)
|
||||
logger.debug("Removing tmpdir '%s'", self.root_dir)
|
||||
remove(self.root_dir)
|
||||
@@ -229,10 +233,7 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
)
|
||||
)
|
||||
merge_rev = self.scm.get_ref(EXEC_MERGE)
|
||||
try:
|
||||
self.scm.merge(merge_rev, squash=True, commit=False)
|
||||
except _SCMError as exc:
|
||||
raise GitMergeError(str(exc), scm=self.scm) # noqa: B904
|
||||
self.scm.stash.apply(merge_rev)
|
||||
if branch:
|
||||
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
|
||||
elif scm.get_ref(EXEC_BRANCH):
|
||||
@@ -241,9 +242,10 @@ class WorkspaceExecutor(BaseLocalExecutor):
|
||||
def init_cache(self, repo: "Repo", rev: str, run_cache: bool = True):
|
||||
pass
|
||||
|
||||
def cleanup(self, infofile: str):
|
||||
def cleanup(self, infofile: Optional[str] = None):
|
||||
super().cleanup(infofile)
|
||||
remove(os.path.dirname(infofile))
|
||||
if infofile:
|
||||
remove(os.path.dirname(infofile))
|
||||
with self._detach_stack:
|
||||
self.scm.remove_ref(EXEC_BASELINE)
|
||||
self.scm.remove_ref(EXEC_MERGE)
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import posixpath
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional
|
||||
|
||||
from dvc_ssh import SSHFileSystem
|
||||
from funcy import first
|
||||
@@ -169,7 +169,7 @@ class SSHExecutor(BaseExecutor):
|
||||
head = EXEC_BRANCH if branch else EXEC_HEAD
|
||||
self._ssh_cmd(fs, f"git checkout {head}")
|
||||
merge_rev = scm.get_ref(EXEC_MERGE)
|
||||
self._ssh_cmd(fs, f"git merge --squash --no-commit {merge_rev}")
|
||||
self._ssh_cmd(fs, f"git stash apply {merge_rev}")
|
||||
|
||||
if self._setup_script:
|
||||
self._init_setup_script(fs)
|
||||
@@ -217,12 +217,12 @@ class SSHExecutor(BaseExecutor):
|
||||
|
||||
@contextmanager
|
||||
def get_odb(self):
|
||||
from dvc.odbmgr import ODBManager, get_odb
|
||||
from dvc.cachemgr import CacheManager, get_odb
|
||||
|
||||
cache_path = posixpath.join(
|
||||
self._repo_abspath,
|
||||
self.dvc_dir,
|
||||
ODBManager.CACHE_DIR,
|
||||
CacheManager.CACHE_DIR,
|
||||
)
|
||||
|
||||
with self.sshfs() as fs:
|
||||
@@ -242,6 +242,8 @@ class SSHExecutor(BaseExecutor):
|
||||
infofile: Optional[str] = None,
|
||||
log_errors: bool = True,
|
||||
log_level: Optional[int] = None,
|
||||
copy_paths: Optional[List[str]] = None, # noqa: ARG003
|
||||
message: Optional[str] = None, # noqa: ARG003
|
||||
**kwargs,
|
||||
) -> "ExecutorResult":
|
||||
"""Reproduce an experiment on a remote machine over SSH.
|
||||
|
||||
@@ -21,9 +21,9 @@ from dvc.exceptions import DvcException
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.dependency import Dependency
|
||||
from dvc.dvcfile import ProjectFile, SingleStageFile
|
||||
from dvc.repo import Repo
|
||||
from dvc.dependency import Dependency
|
||||
|
||||
from dvc.ui import ui
|
||||
|
||||
@@ -218,13 +218,12 @@ def init(
|
||||
provided=overrides,
|
||||
stream=stream,
|
||||
)
|
||||
elif "live" in overrides:
|
||||
# suppress `metrics`/`plots` if live is selected.
|
||||
defaults.pop("metrics", None)
|
||||
defaults.pop("plots", None)
|
||||
else:
|
||||
if "live" in overrides:
|
||||
# suppress `metrics`/`plots` if live is selected.
|
||||
defaults.pop("metrics", None)
|
||||
defaults.pop("plots", None)
|
||||
else:
|
||||
defaults.pop("live", None) # suppress live otherwise
|
||||
defaults.pop("live", None) # suppress live otherwise
|
||||
|
||||
context: Dict[str, str] = {**defaults, **overrides}
|
||||
assert "cmd" in context
|
||||
|
||||
+124
-54
@@ -1,79 +1,148 @@
|
||||
import logging
|
||||
from typing import Iterable, List, Mapping, Optional, Set, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Set,
|
||||
Union,
|
||||
)
|
||||
|
||||
from funcy import group_by
|
||||
from funcy import compact, group_by
|
||||
from scmrepo.git.backend.base import SyncStatus
|
||||
|
||||
from dvc.env import DVC_STUDIO_TOKEN, DVC_STUDIO_URL
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo import locked
|
||||
from dvc.repo.scm_context import scm_context
|
||||
from dvc.scm import TqdmGit, iter_revs
|
||||
from dvc.ui import ui
|
||||
from dvc.scm import Git, TqdmGit, iter_revs
|
||||
from dvc.utils import env2bool
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
from .exceptions import UnresolvedExpNamesError
|
||||
from .refs import ExpRefInfo
|
||||
from .utils import exp_commits, exp_refs, exp_refs_by_baseline, resolve_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UploadError(DvcException):
|
||||
def __init__(self, msg, result):
|
||||
self.result = result
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
def notify_refs_to_studio(
|
||||
repo: "Repo", git_remote: str, **refs: List[str]
|
||||
) -> Optional[str]:
|
||||
import os
|
||||
|
||||
config = repo.config["studio"]
|
||||
refs = compact(refs)
|
||||
if not refs or env2bool("DVC_TEST"):
|
||||
return None
|
||||
|
||||
token = (
|
||||
os.environ.get(DVC_STUDIO_TOKEN)
|
||||
or os.environ.get("STUDIO_TOKEN")
|
||||
or config.get("token")
|
||||
)
|
||||
if not token:
|
||||
logger.debug("Studio token not found.")
|
||||
return None
|
||||
|
||||
from dulwich.porcelain import get_remote_repo
|
||||
|
||||
from dvc.utils import studio
|
||||
|
||||
_, repo_url = get_remote_repo(repo.scm.dulwich.repo, git_remote)
|
||||
studio_url = os.environ.get(DVC_STUDIO_URL) or config.get("url")
|
||||
d = studio.notify_refs(repo_url, token, base_url=studio_url, **refs)
|
||||
return d.get("url")
|
||||
|
||||
|
||||
def exp_refs_from_names(scm: "Git", exp_names: List[str]) -> Set["ExpRefInfo"]:
|
||||
exp_ref_set = set()
|
||||
exp_ref_dict = resolve_name(scm, exp_names)
|
||||
unresolved_exp_names = []
|
||||
for exp_name, exp_ref in exp_ref_dict.items():
|
||||
if exp_ref is None:
|
||||
unresolved_exp_names.append(exp_name)
|
||||
else:
|
||||
exp_ref_set.add(exp_ref)
|
||||
|
||||
if unresolved_exp_names:
|
||||
raise UnresolvedExpNamesError(unresolved_exp_names)
|
||||
return exp_ref_set
|
||||
|
||||
|
||||
def exp_refs_from_rev(scm: "Git", rev: str, num: int = 1) -> Set["ExpRefInfo"]:
|
||||
exp_ref_set = set()
|
||||
rev_dict = iter_revs(scm, [rev], num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(scm, rev_set)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
return exp_ref_set
|
||||
|
||||
|
||||
@locked
|
||||
@scm_context
|
||||
def push( # noqa: C901
|
||||
repo,
|
||||
def push(
|
||||
repo: "Repo",
|
||||
git_remote: str,
|
||||
exp_names: Union[Iterable[str], str],
|
||||
all_commits=False,
|
||||
exp_names: Union[List[str], str],
|
||||
all_commits: bool = False,
|
||||
rev: Optional[str] = None,
|
||||
num=1,
|
||||
num: int = 1,
|
||||
force: bool = False,
|
||||
push_cache: bool = False,
|
||||
**kwargs,
|
||||
) -> Iterable[str]:
|
||||
**kwargs: Any,
|
||||
) -> Dict[str, Any]:
|
||||
exp_ref_set: Set["ExpRefInfo"] = set()
|
||||
assert isinstance(repo.scm, Git)
|
||||
if all_commits:
|
||||
exp_ref_set.update(exp_refs(repo.scm))
|
||||
|
||||
else:
|
||||
if exp_names:
|
||||
if isinstance(exp_names, str):
|
||||
exp_names = [exp_names]
|
||||
exp_ref_dict = resolve_name(repo.scm, exp_names)
|
||||
|
||||
unresolved_exp_names = []
|
||||
for exp_name, exp_ref in exp_ref_dict.items():
|
||||
if exp_ref is None:
|
||||
unresolved_exp_names.append(exp_name)
|
||||
else:
|
||||
exp_ref_set.add(exp_ref)
|
||||
|
||||
if unresolved_exp_names:
|
||||
raise UnresolvedExpNamesError(unresolved_exp_names)
|
||||
|
||||
if rev:
|
||||
rev_dict = iter_revs(repo.scm, [rev], num)
|
||||
rev_set = set(rev_dict.keys())
|
||||
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set)
|
||||
for _, ref_info_list in ref_info_dict.items():
|
||||
exp_ref_set.update(ref_info_list)
|
||||
if exp_names:
|
||||
exp_ref_set.update(exp_refs_from_names(repo.scm, ensure_list(exp_names)))
|
||||
if rev:
|
||||
exp_ref_set.update(exp_refs_from_rev(repo.scm, rev, num=num))
|
||||
|
||||
push_result = _push(repo, git_remote, exp_ref_set, force)
|
||||
if push_result[SyncStatus.DIVERGED]:
|
||||
diverged_refs = [ref.name for ref in push_result[SyncStatus.DIVERGED]]
|
||||
ui.warn(
|
||||
f"Local experiment '{diverged_refs}' has diverged from remote "
|
||||
"experiment with the same name. To override the remote experiment "
|
||||
"re-run with '--force'."
|
||||
)
|
||||
|
||||
refs = {
|
||||
status.name.lower(): [ref.name for ref in ref_list]
|
||||
for status, ref_list in push_result.items()
|
||||
}
|
||||
result: Dict[str, Any] = {**refs, "uploaded": 0}
|
||||
|
||||
pushed_refs_info = (
|
||||
push_result[SyncStatus.UP_TO_DATE] + push_result[SyncStatus.SUCCESS]
|
||||
)
|
||||
|
||||
e = None
|
||||
if push_cache:
|
||||
push_cache_ref = (
|
||||
push_result[SyncStatus.UP_TO_DATE] + push_result[SyncStatus.SUCCESS]
|
||||
)
|
||||
_push_cache(repo, push_cache_ref, **kwargs)
|
||||
return [ref.name for ref in push_result[SyncStatus.SUCCESS]]
|
||||
try:
|
||||
result["uploaded"] = _push_cache(repo, pushed_refs_info, **kwargs)
|
||||
except Exception as exc: # noqa: BLE001, pylint: disable=broad-except
|
||||
e = exc
|
||||
|
||||
pushed_refs = [str(r) for r in pushed_refs_info]
|
||||
result["url"] = notify_refs_to_studio(repo, git_remote, pushed=pushed_refs)
|
||||
|
||||
if e:
|
||||
raise UploadError("failed to push cache", result) from e
|
||||
return result
|
||||
|
||||
|
||||
def _push(
|
||||
repo,
|
||||
repo: "Repo",
|
||||
git_remote: str,
|
||||
refs: Iterable["ExpRefInfo"],
|
||||
force: bool,
|
||||
@@ -83,7 +152,7 @@ def _push(
|
||||
from dvc.scm import GitAuthError
|
||||
|
||||
refspec_list = [f"{exp_ref}:{exp_ref}" for exp_ref in refs]
|
||||
logger.debug("git push experiment '%s' -> '%s'", refspec_list, git_remote)
|
||||
logger.debug("git push experiment %s -> '%s'", refspec_list, git_remote)
|
||||
|
||||
with TqdmGit(desc="Pushing git refs") as pbar:
|
||||
try:
|
||||
@@ -105,14 +174,15 @@ def _push(
|
||||
|
||||
|
||||
def _push_cache(
|
||||
repo,
|
||||
repo: "Repo",
|
||||
refs: Union[ExpRefInfo, Iterable["ExpRefInfo"]],
|
||||
dvc_remote=None,
|
||||
jobs=None,
|
||||
run_cache=False,
|
||||
):
|
||||
dvc_remote: Optional[str] = None,
|
||||
jobs: Optional[int] = None,
|
||||
run_cache: bool = False,
|
||||
) -> int:
|
||||
if isinstance(refs, ExpRefInfo):
|
||||
refs = [refs]
|
||||
assert isinstance(repo.scm, Git)
|
||||
revs = list(exp_commits(repo.scm, refs))
|
||||
logger.debug("dvc push experiment '%s'", refs)
|
||||
repo.push(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
return repo.push(jobs=jobs, remote=dvc_remote, run_cache=run_cache, revs=revs)
|
||||
|
||||
@@ -17,6 +17,7 @@ from typing import (
|
||||
Union,
|
||||
)
|
||||
|
||||
from dvc_studio_client.post_live_metrics import get_studio_config
|
||||
from funcy import retry
|
||||
|
||||
from dvc.dependency import ParamsDependency
|
||||
@@ -37,6 +38,7 @@ from dvc.repo.experiments.utils import (
|
||||
)
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc.utils.studio import config_to_env
|
||||
|
||||
from .utils import get_remote_executor_refs
|
||||
|
||||
@@ -44,6 +46,7 @@ if TYPE_CHECKING:
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments import Experiments
|
||||
from dvc.repo.experiments.executor.base import ExecutorResult
|
||||
from dvc.repo.experiments.serialize import ExpRange
|
||||
from dvc.scm import Git
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -250,7 +253,9 @@ class BaseStashQueue(ABC):
|
||||
"""Iterate over items which been failed."""
|
||||
|
||||
@abstractmethod
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
def reproduce(
|
||||
self, copy_paths: Optional[List[str]] = None, message: Optional[str] = None
|
||||
) -> Mapping[str, Mapping[str, str]]:
|
||||
"""Reproduce queued experiments sequentially."""
|
||||
|
||||
@abstractmethod
|
||||
@@ -295,7 +300,7 @@ class BaseStashQueue(ABC):
|
||||
output.
|
||||
"""
|
||||
|
||||
def _stash_exp( # noqa: C901
|
||||
def _stash_exp( # noqa: PLR0915, C901
|
||||
self,
|
||||
*args,
|
||||
params: Optional[Dict[str, List[str]]] = None,
|
||||
@@ -323,12 +328,12 @@ class BaseStashQueue(ABC):
|
||||
.. _Hydra Override:
|
||||
https://hydra.cc/docs/next/advanced/override_grammar/basic/
|
||||
"""
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
with self.scm.stash_workspace(reinstate_index=True) as workspace:
|
||||
with self.scm.detach_head(client="dvc") as orig_head:
|
||||
stash_head = orig_head
|
||||
if baseline_rev is None:
|
||||
baseline_rev = orig_head
|
||||
|
||||
with self.scm.stash_workspace() as workspace:
|
||||
try:
|
||||
if workspace:
|
||||
self.stash.apply(workspace)
|
||||
@@ -415,8 +420,13 @@ class BaseStashQueue(ABC):
|
||||
run_env[DVC_EXP_NAME] = name
|
||||
if resume_rev:
|
||||
run_env[DVCLIVE_RESUME] = "1"
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
|
||||
# save studio config to read later by dvc and dvclive
|
||||
studio_config = get_studio_config(
|
||||
dvc_studio_config=self.repo.config.get("studio")
|
||||
)
|
||||
run_env = {**config_to_env(studio_config), **run_env}
|
||||
self._pack_args(*args, run_env=run_env, **kwargs)
|
||||
# save experiment as a stash commit
|
||||
msg = self._stash_msg(
|
||||
stash_head,
|
||||
@@ -472,6 +482,7 @@ class BaseStashQueue(ABC):
|
||||
force=True,
|
||||
allow_missing=True,
|
||||
data_only=True,
|
||||
relink=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -489,7 +500,7 @@ class BaseStashQueue(ABC):
|
||||
return msg
|
||||
|
||||
def _pack_args(self, *args, **kwargs) -> None:
|
||||
import pickle
|
||||
import pickle # nosec B403
|
||||
|
||||
if os.path.exists(self.args_file) and self.scm.is_tracked(self.args_file):
|
||||
logger.warning(
|
||||
@@ -504,14 +515,14 @@ class BaseStashQueue(ABC):
|
||||
)
|
||||
with open(self.args_file, "rb") as fobj:
|
||||
try:
|
||||
data = pickle.load(fobj)
|
||||
data = pickle.load(fobj) # noqa: S301 # nosec B301
|
||||
except Exception: # noqa: BLE001, pylint: disable=broad-except
|
||||
data = {}
|
||||
extra = int(data.get("extra", 0)) + 1
|
||||
else:
|
||||
extra = None
|
||||
BaseExecutor.pack_repro_args(self.args_file, *args, extra=extra, **kwargs)
|
||||
self.scm.add(self.args_file)
|
||||
self.scm.add(self.args_file, force=True)
|
||||
|
||||
@staticmethod
|
||||
def _format_new_params_msg(new_params, config_path):
|
||||
@@ -725,3 +736,57 @@ class BaseStashQueue(ABC):
|
||||
Args:
|
||||
fetch_ref (bool): fetch completed checkpoints or not.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def collect_active_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
fetch_refs: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect data for active (running) experiments.
|
||||
|
||||
Args:
|
||||
baseline_revs: Optional resolved baseline Git SHAs. If set, only experiments
|
||||
derived from the specified revisions will be collected. Defaults to
|
||||
collecting all experiments.
|
||||
fetch_refs: Whether or not to fetch completed checkpoint commits from Git
|
||||
remote.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of active experiments.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def collect_queued_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect data for queued experiments.
|
||||
|
||||
Args:
|
||||
baseline_revs: Optional resolved baseline Git SHAs. If set, only experiments
|
||||
derived from the specified revisions will be collected. Defaults to
|
||||
collecting all experiments.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of queued experiments.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def collect_failed_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
"""Collect data for failed experiments.
|
||||
|
||||
Args:
|
||||
baseline_revs: Optional resolved baseline Git SHAs. If set, only experiments
|
||||
derived from the specified revisions will be collected. Defaults to
|
||||
collecting all experiments.
|
||||
|
||||
Returns:
|
||||
Dict mapping baseline revision to list of queued experiments.
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,7 @@ import hashlib
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Collection,
|
||||
@@ -40,6 +41,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from dvc.repo.experiments.executor.base import ExecutorResult
|
||||
from dvc.repo.experiments.refs import ExpRefInfo
|
||||
from dvc.repo.experiments.serialize import ExpRange
|
||||
from dvc_task.app import FSApp
|
||||
from dvc_task.proc.manager import ProcessManager
|
||||
from dvc_task.worker import TemporaryWorker
|
||||
@@ -90,7 +92,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
"dvc_task.proc.tasks",
|
||||
],
|
||||
)
|
||||
app.conf.update({"task_acks_late": True})
|
||||
app.conf.update({"task_acks_late": True, "result_expires": None})
|
||||
return app
|
||||
|
||||
@cached_property
|
||||
@@ -123,6 +125,7 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
without_mingle=True,
|
||||
without_gossip=True,
|
||||
timeout=10,
|
||||
loglevel="debug" if logger.getEffectiveLevel() <= logging.DEBUG else "info",
|
||||
)
|
||||
|
||||
def _spawn_worker(self, num: int = 1):
|
||||
@@ -138,6 +141,11 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
wdir_hash = hashlib.sha256(self.wdir.encode("utf-8")).hexdigest()[:6]
|
||||
node_name = f"dvc-exp-{wdir_hash}-{num}@localhost"
|
||||
cmd = ["exp", "queue-worker", node_name]
|
||||
if num == 1:
|
||||
# automatically run celery cleanup when primary worker shuts down
|
||||
cmd.append("--clean")
|
||||
if logger.getEffectiveLevel() <= logging.DEBUG:
|
||||
cmd.append("-v")
|
||||
name = f"dvc-exp-worker-{num}"
|
||||
|
||||
logger.debug("start a new worker: %s, node: %s", name, node_name)
|
||||
@@ -171,11 +179,19 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
|
||||
return started
|
||||
|
||||
def put(self, *args, **kwargs) -> QueueEntry:
|
||||
def put(
|
||||
self,
|
||||
*args,
|
||||
copy_paths: Optional[List[str]] = None,
|
||||
message: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> QueueEntry:
|
||||
"""Stash an experiment and add it to the queue."""
|
||||
with get_exp_rwlock(self.repo, writes=["workspace", CELERY_STASH]):
|
||||
entry = self._stash_exp(*args, **kwargs)
|
||||
self.celery.signature(run_exp.s(entry.asdict())).delay()
|
||||
self.celery.signature(
|
||||
run_exp.s(entry.asdict(), copy_paths=copy_paths, message=message)
|
||||
).delay()
|
||||
return entry
|
||||
|
||||
# NOTE: Queue consumption should not be done directly. Celery worker(s)
|
||||
@@ -193,7 +209,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
continue
|
||||
args, kwargs, _embed = msg.decode()
|
||||
entry_dict = kwargs.get("entry_dict", args[0])
|
||||
logger.debug("Found queued task %s", entry_dict["stash_rev"])
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Found queued task %s", entry_dict["stash_rev"]
|
||||
)
|
||||
yield _MessageEntry(msg, QueueEntry.from_dict(entry_dict))
|
||||
|
||||
def _iter_processed(self) -> Generator[_MessageEntry, None, None]:
|
||||
@@ -209,7 +227,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if not result.ready():
|
||||
logger.debug("Found active task %s", entry.stash_rev)
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Found active task %s", entry.stash_rev
|
||||
)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def _iter_done_tasks(self) -> Generator[_TaskEntry, None, None]:
|
||||
@@ -217,7 +237,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id = msg.headers["id"]
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result.ready():
|
||||
logger.debug("Found done task %s", entry.stash_rev)
|
||||
logger.trace( # type: ignore[attr-defined]
|
||||
"Found done task %s", entry.stash_rev
|
||||
)
|
||||
yield _TaskEntry(result, entry)
|
||||
|
||||
def iter_active(self) -> Generator[QueueEntry, None, None]:
|
||||
@@ -247,7 +269,9 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
if exp_result is None:
|
||||
yield QueueDoneResult(queue_entry, exp_result)
|
||||
|
||||
def reproduce(self) -> Mapping[str, Mapping[str, str]]:
|
||||
def reproduce(
|
||||
self, copy_paths: Optional[List[str]] = None, message: Optional[str] = None
|
||||
) -> Mapping[str, Mapping[str, str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
def _load_info(self, rev: str) -> ExecutorInfo:
|
||||
@@ -292,6 +316,28 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
# out of the active task list, and needs to be loaded here.
|
||||
return self._get_done_result(entry, timeout)
|
||||
|
||||
def wait(self, revs: Collection[str], **kwargs) -> None:
|
||||
"""Block until the specified tasks have completed."""
|
||||
revs = [revs] if isinstance(revs, str) else revs
|
||||
results = self.match_queue_entry_by_name(
|
||||
revs, self.iter_queued(), self.iter_done(), self.iter_failed()
|
||||
)
|
||||
for entry in results.values():
|
||||
if not entry:
|
||||
continue
|
||||
self.wait_for_start(entry, **kwargs)
|
||||
try:
|
||||
self.get_result(entry)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def wait_for_start(self, entry: QueueEntry, sleep_interval: float = 0.001) -> None:
|
||||
"""Block until the specified task has been started."""
|
||||
import time
|
||||
|
||||
while not self.proc.get(entry.stash_rev):
|
||||
time.sleep(sleep_interval)
|
||||
|
||||
def _get_running_task_ids(self) -> Set[str]:
|
||||
running_task_ids: Set[str] = set()
|
||||
active_workers = self.worker_status()
|
||||
@@ -336,7 +382,8 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
task_id,
|
||||
remained_entries[entry],
|
||||
)
|
||||
self.celery.backend.mark_as_failure(task_id, None)
|
||||
backend = self.celery.backend
|
||||
backend.mark_as_failure(task_id, None) # type: ignore[attr-defined]
|
||||
|
||||
if remained_revs:
|
||||
raise CannotKillTasksError(remained_revs)
|
||||
@@ -482,3 +529,124 @@ class LocalCeleryQueue(BaseStashQueue):
|
||||
queue_entry = None if git_remote else queue_entry_match[exp_name]
|
||||
results[exp_name] = ExpRefAndQueueEntry(exp_ref, queue_entry)
|
||||
return results
|
||||
|
||||
def collect_active_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
fetch_refs: bool = False,
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.experiments.collect import collect_exec_branch
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry in self.iter_active():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
if fetch_refs:
|
||||
fetch_running_exp_from_temp_dir(self, entry.stash_rev, fetch_refs)
|
||||
proc_info = self.proc.get(entry.stash_rev)
|
||||
executor_info = self._load_info(entry.stash_rev)
|
||||
if proc_info:
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
root=executor_info.root_dir,
|
||||
log=proc_info.stdout,
|
||||
pid=proc_info.pid,
|
||||
task_id=entry.stash_rev,
|
||||
)
|
||||
else:
|
||||
local_exec = None
|
||||
dvc_root = os.path.join(executor_info.root_dir, executor_info.dvc_dir)
|
||||
with Repo(dvc_root) as exec_repo:
|
||||
kwargs["cache"] = self.repo.experiments.cache
|
||||
exps = list(
|
||||
collect_exec_branch(exec_repo, executor_info.baseline_rev, **kwargs)
|
||||
)
|
||||
exps[0].rev = entry.stash_rev
|
||||
exps[0].name = entry.name
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
exps,
|
||||
executor=ExpExecutor(
|
||||
"running",
|
||||
name=executor_info.location,
|
||||
local=local_exec,
|
||||
),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def collect_queued_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo.experiments.collect import collect_rev
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry in self.iter_queued():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
exp = collect_rev(self.repo, entry.stash_rev, **kwargs)
|
||||
exp.name = entry.name
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
task_id=entry.stash_rev,
|
||||
)
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
[exp],
|
||||
executor=ExpExecutor("queued", name="dvc-task", local=local_exec),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def collect_failed_data(
|
||||
self,
|
||||
baseline_revs: Optional[Collection[str]],
|
||||
**kwargs,
|
||||
) -> Dict[str, List["ExpRange"]]:
|
||||
from dvc.repo.experiments.collect import collect_rev
|
||||
from dvc.repo.experiments.serialize import (
|
||||
ExpExecutor,
|
||||
ExpRange,
|
||||
LocalExpExecutor,
|
||||
SerializableError,
|
||||
)
|
||||
|
||||
result: Dict[str, List[ExpRange]] = defaultdict(list)
|
||||
for entry, _ in self.iter_failed():
|
||||
if baseline_revs and entry.baseline_rev not in baseline_revs:
|
||||
continue
|
||||
proc_info = self.proc.get(entry.stash_rev)
|
||||
if proc_info:
|
||||
local_exec: Optional[LocalExpExecutor] = LocalExpExecutor(
|
||||
log=proc_info.stdout,
|
||||
pid=proc_info.pid,
|
||||
returncode=proc_info.returncode,
|
||||
task_id=entry.stash_rev,
|
||||
)
|
||||
else:
|
||||
local_exec = None
|
||||
exp = collect_rev(self.repo, entry.stash_rev, **kwargs)
|
||||
exp.name = entry.name
|
||||
exp.error = SerializableError("Experiment run failed")
|
||||
result[entry.baseline_rev].append(
|
||||
ExpRange(
|
||||
[exp],
|
||||
executor=ExpExecutor("failed", local=local_exec),
|
||||
name=entry.name,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||
from dvc.repo.experiments.stash import ExpStashEntry
|
||||
|
||||
|
||||
def remove_tasks( # noqa: C901
|
||||
def remove_tasks( # noqa: C901, PLR0912
|
||||
celery_queue: "LocalCeleryQueue",
|
||||
queue_entries: Iterable["QueueEntry"],
|
||||
):
|
||||
@@ -40,7 +40,7 @@ def remove_tasks( # noqa: C901
|
||||
msg,
|
||||
queue_entry,
|
||||
) in celery_queue._iter_queued(): # pylint: disable=protected-access
|
||||
if queue_entry.stash_rev in stash_revs:
|
||||
if queue_entry.stash_rev in stash_revs and msg.delivery_tag:
|
||||
celery_queue.celery.reject(msg.delivery_tag)
|
||||
finally:
|
||||
celery_queue.stash.remove_revs(list(stash_revs.values()))
|
||||
@@ -56,7 +56,8 @@ def remove_tasks( # noqa: C901
|
||||
result: AsyncResult = AsyncResult(task_id)
|
||||
if result is not None:
|
||||
result.forget()
|
||||
celery_queue.celery.purge(msg.delivery_tag)
|
||||
if msg.delivery_tag:
|
||||
celery_queue.celery.purge(msg.delivery_tag)
|
||||
finally:
|
||||
if celery_queue.failed_stash:
|
||||
celery_queue.failed_stash.remove_revs(failed_stash_revs)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user