mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
285
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5ae8b19a4 | ||
|
|
f2b1c41cdf | ||
|
|
ffd32dfe8a | ||
|
|
2b19c724c5 | ||
|
|
28b92c8b03 | ||
|
|
cd740fed42 | ||
|
|
5f31ed2b08 | ||
|
|
6e896d396c | ||
|
|
336c8695da | ||
|
|
3915fa26aa | ||
|
|
114fd8075c | ||
|
|
b39a888948 | ||
|
|
c1af66c0a6 | ||
|
|
75c5d17d1e | ||
|
|
ea9dd5fc3e | ||
|
|
763cbf6bd1 | ||
|
|
6f4b8c98fc | ||
|
|
a8bda305cd | ||
|
|
4f4c5b1d01 | ||
|
|
76cfe92960 | ||
|
|
79454557bb | ||
|
|
85046843ef | ||
|
|
e10b27acbd | ||
|
|
5a86586f3a | ||
|
|
d142d0d6af | ||
|
|
9b5ee43ce1 | ||
|
|
6e223633e7 | ||
|
|
29386a0dbb | ||
|
|
e027bc6a87 | ||
|
|
82efd3e780 | ||
|
|
a68dda4164 | ||
|
|
4b1adfd646 | ||
|
|
748c45655e | ||
|
|
225bfa6a19 | ||
|
|
09079d50ff | ||
|
|
5485e5662e | ||
|
|
e619b6dccd | ||
|
|
396e71f30c | ||
|
|
c6a393e3fe | ||
|
|
a72402c843 | ||
|
|
08f3197cdd | ||
|
|
1f325bc21e | ||
|
|
ca73078b80 | ||
|
|
fade6c5991 | ||
|
|
446a59c806 | ||
|
|
a370f80f90 | ||
|
|
dc13186759 | ||
|
|
ba1281a41c | ||
|
|
abb5977a38 | ||
|
|
50487e931f | ||
|
|
397f4c6f28 | ||
|
|
1f5bace250 | ||
|
|
5a68e821f2 | ||
|
|
b4d8690799 | ||
|
|
7f0c835eed | ||
|
|
c2506abf74 | ||
|
|
d938a1c700 | ||
|
|
a0b6b12a4f | ||
|
|
1b9b1f870a | ||
|
|
be1b529bdf | ||
|
|
da55c0da1f | ||
|
|
a23810f0b5 | ||
|
|
0adf6679d9 | ||
|
|
75083cc472 | ||
|
|
493279c7e8 | ||
|
|
95c5ea81e0 | ||
|
|
3aca0edfce | ||
|
|
5bec7d1bb9 | ||
|
|
f3836ab29a | ||
|
|
e278c9cdf4 | ||
|
|
aac32e1b64 | ||
|
|
45fd1025e2 | ||
|
|
c7c7ba69fb | ||
|
|
346b1c064e | ||
|
|
b89cb893a1 | ||
|
|
549821e6be | ||
|
|
bf26126e37 | ||
|
|
8f1c4424d3 | ||
|
|
5fb8986dbd | ||
|
|
18177a6579 | ||
|
|
ea02a18b23 | ||
|
|
e04f1499a3 | ||
|
|
488c30d1ea | ||
|
|
88c3103cf3 | ||
|
|
c37bef7add | ||
|
|
7ea1daa2b8 | ||
|
|
fd181c78d2 | ||
|
|
1de7f89af8 | ||
|
|
b7e13dbd07 | ||
|
|
dc513a98e4 | ||
|
|
9908ca7f8e | ||
|
|
0eebeb9900 | ||
|
|
415d4bc980 | ||
|
|
f3be7d761f | ||
|
|
4bde0e754e | ||
|
|
88f1ada4bf | ||
|
|
bd65c205df | ||
|
|
e6ce5f825d | ||
|
|
d30d66c1d3 | ||
|
|
19f31d7cac | ||
|
|
99d9ca714d | ||
|
|
a3b6722523 | ||
|
|
b824bc4db7 | ||
|
|
6cc349edb4 | ||
|
|
e455ee69aa | ||
|
|
31270c14e3 | ||
|
|
28b7c8b723 | ||
|
|
1d6ea68133 | ||
|
|
25d2638a57 | ||
|
|
0b03714097 | ||
|
|
9392bbdd4b | ||
|
|
c4b18b710a | ||
|
|
da1e0b57b8 | ||
|
|
b2a873d13b | ||
|
|
89ef3463dc | ||
|
|
7d14acb5a7 | ||
|
|
dd501554fe | ||
|
|
54acfda687 | ||
|
|
e70a1e9bfc | ||
|
|
aea0e6b6cc | ||
|
|
2c6b917fc5 | ||
|
|
a43c1b31f7 | ||
|
|
683a9f09bd | ||
|
|
53a7230da0 | ||
|
|
f078a572dd | ||
|
|
007041bfe9 | ||
|
|
965dbea75e | ||
|
|
f5cbf97eea | ||
|
|
198c8f2ffd | ||
|
|
368c785410 | ||
|
|
64ccd9cfdb | ||
|
|
d38b2dbcb8 | ||
|
|
8cb0f02bf9 | ||
|
|
7e75b0616f | ||
|
|
fb24775bc0 | ||
|
|
82e2446eb4 | ||
|
|
1e08cc5da3 | ||
|
|
e56a729e3b | ||
|
|
9a050684f7 | ||
|
|
2431ec6189 | ||
|
|
70fc4e6cda | ||
|
|
bbc01b40d2 | ||
|
|
6302e5f1d3 | ||
|
|
df5429f7d8 | ||
|
|
f9583f0c3d | ||
|
|
9e26a46323 | ||
|
|
746f028f6c | ||
|
|
3651939e11 | ||
|
|
d236667048 | ||
|
|
d22a68f7a8 | ||
|
|
683ae7a951 | ||
|
|
d5054ce861 | ||
|
|
e884f1b8f0 | ||
|
|
9b5772fab8 | ||
|
|
c1aaeffc2d | ||
|
|
ad70dd322b | ||
|
|
72b6971730 | ||
|
|
ee1864af8e | ||
|
|
52877485d1 | ||
|
|
a923f110e3 | ||
|
|
b8266ed15f | ||
|
|
2c871ba432 | ||
|
|
32d11f1e05 | ||
|
|
f56343d6ae | ||
|
|
7b8922b20f | ||
|
|
436a2ac793 | ||
|
|
eec3d48731 | ||
|
|
5417ccd7b6 | ||
|
|
f2b7b00af5 | ||
|
|
4f3fb15f2e | ||
|
|
016f285fe3 | ||
|
|
f6dd641855 | ||
|
|
3f2584d155 | ||
|
|
705b7bbea6 | ||
|
|
26ad01bec0 | ||
|
|
e76aaeac57 | ||
|
|
6f24d04b5e | ||
|
|
2dcaea8119 | ||
|
|
81db9b4572 | ||
|
|
98dde6cd27 | ||
|
|
e42d094310 | ||
|
|
38ee0a01f7 | ||
|
|
d494126cc5 | ||
|
|
fd3b43df05 | ||
|
|
b9bcb45aaa | ||
|
|
7bca71db2e | ||
|
|
52405a0e10 | ||
|
|
c514b0ea31 | ||
|
|
853116a0e3 | ||
|
|
5e4daf6cfb | ||
|
|
991a816995 | ||
|
|
9735b5aaa9 | ||
|
|
fa317d5b1d | ||
|
|
1b0fb6487b | ||
|
|
43e6229308 | ||
|
|
1bef0da4fb | ||
|
|
284d83f390 | ||
|
|
417e6d4b76 | ||
|
|
248ba3b6b6 | ||
|
|
8a91c2b303 | ||
|
|
544dad75e9 | ||
|
|
8f50f2509f | ||
|
|
cdc1857559 | ||
|
|
358dd163da | ||
|
|
aae1a93cb5 | ||
|
|
c9b7218659 | ||
|
|
88c4b9525d | ||
|
|
69d36eeb06 | ||
|
|
3201794553 | ||
|
|
4464f8b4b9 | ||
|
|
efc7e05893 | ||
|
|
a555c3d29a | ||
|
|
676645b1ca | ||
|
|
93c3100bad | ||
|
|
c74156288d | ||
|
|
0e38ccc031 | ||
|
|
b69b368ffe | ||
|
|
6cc80fdeaa | ||
|
|
8f8d49d023 | ||
|
|
83ec952da1 | ||
|
|
c0d7e2a8de | ||
|
|
9aad09164c | ||
|
|
1f7733fec1 | ||
|
|
856c902b65 | ||
|
|
ebb459f9e2 | ||
|
|
2e0b024efe | ||
|
|
135fa26515 | ||
|
|
ef507efa04 | ||
|
|
05db79e7cf | ||
|
|
0a813bbcbc | ||
|
|
e82356ba92 | ||
|
|
5bfacec974 | ||
|
|
1848074ac9 | ||
|
|
a7c0f6ef39 | ||
|
|
0a8a9deceb | ||
|
|
c5d98681c0 | ||
|
|
15e05e4b3e | ||
|
|
7df8b0c09b | ||
|
|
2e1faf2979 | ||
|
|
cbaeff2ea1 | ||
|
|
132e625ae8 | ||
|
|
8517cccff4 | ||
|
|
c52a25e011 | ||
|
|
c4678736b0 | ||
|
|
ec92cc14d1 | ||
|
|
e6048287cb | ||
|
|
72a353d8b9 | ||
|
|
dd2b515c97 | ||
|
|
c223cdaecd | ||
|
|
5379aa2348 | ||
|
|
3375a7c39f | ||
|
|
519c47f5fc | ||
|
|
6f6038888f | ||
|
|
e223f51e7e | ||
|
|
fb628bf9ea | ||
|
|
4f4e977e53 | ||
|
|
8b1f8847fe | ||
|
|
c62a0380e9 | ||
|
|
8d8939fb67 | ||
|
|
f7a54bf2d0 | ||
|
|
1a9f5d3aa8 | ||
|
|
017a510b83 | ||
|
|
a80d224c58 | ||
|
|
49709a4810 | ||
|
|
7df58ce404 | ||
|
|
827e58f305 | ||
|
|
8bc6ba7cd5 | ||
|
|
a96a446742 | ||
|
|
837c8e8de4 | ||
|
|
603da1ca1e | ||
|
|
377d0c9d58 | ||
|
|
6bdf41a217 | ||
|
|
1aeb8750b6 | ||
|
|
5aae934379 | ||
|
|
de1338caaf | ||
|
|
9957410998 | ||
|
|
cf9855af45 | ||
|
|
0001cc3a4b | ||
|
|
440e1491f0 | ||
|
|
b0384f8f51 | ||
|
|
cd1e228e55 | ||
|
|
3b2e031e60 | ||
|
|
e5ee82bc99 | ||
|
|
bc95b650a4 | ||
|
|
257ea5f4cb |
File diff suppressed because it is too large
Load Diff
@@ -1,60 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,10 +0,0 @@
|
||||
pull_request_rules:
|
||||
- name: backport patches to 2.x
|
||||
conditions:
|
||||
- base=main
|
||||
- label=backport
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- "2.x"
|
||||
title: ({{ destination_branch }}) {{ title }}
|
||||
@@ -5,14 +5,40 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FORCE_COLOR: "1"
|
||||
PY_COLORS: "1"
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- uses: iterative/dvc-bench@main
|
||||
python-version: "3.13"
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
pytest_options: "-k 'test_init or test_help'"
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: pyproject.toml
|
||||
|
||||
- run: uv pip install '.[tests]' --system
|
||||
- name: run benchmarks on base branch
|
||||
run: pytest --benchmark-autosave dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
clean: false
|
||||
- run: uv pip install '.[tests]' --system
|
||||
- name: run benchmarks for PR
|
||||
run: >
|
||||
pytest --benchmark-compare --benchmark-compare-fail=min:5%
|
||||
--benchmark-group-by name
|
||||
dvc/testing/benchmarks/ -k 'test_init or test_help'
|
||||
|
||||
@@ -19,30 +19,33 @@ jobs:
|
||||
name: Build & Verify package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.12
|
||||
python-version: 3.13
|
||||
|
||||
- run: python -m pip install --upgrade pip setuptools_scm build twine
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Force version for Test PyPI upload
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags') }}
|
||||
run: echo version=$(python -m setuptools_scm | awk -F+ '{print $1}' | tail -1) >> $GITHUB_ENV
|
||||
- name: Omit local version for Test PyPI upload
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
run: echo SETUPTOOLS_SCM_OVERRIDES_FOR_DVC='{local_scheme="no-local-version"}' >> $GITHUB_ENV
|
||||
|
||||
- run: echo 'PKG = "pip"'>dvc/_build.py
|
||||
- run: python -m build
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
|
||||
- run: twine check --strict dist/*
|
||||
- name: Build Python Package
|
||||
run: |
|
||||
echo 'PKG = "pip"'>dvc/_build.py
|
||||
uv build
|
||||
|
||||
- name: Check dist
|
||||
run: uv tool run twine check --strict dist/*
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Packages
|
||||
path: dist/
|
||||
if-no-files-found: error
|
||||
|
||||
test-pypi-publish:
|
||||
name: Publish dev package to test.pypi.org
|
||||
@@ -52,13 +55,13 @@ jobs:
|
||||
|
||||
environment:
|
||||
name: test-pypi
|
||||
url: https://test.pypi.org/p/dvc
|
||||
url: https://test.pypi.org/p/dvc/${{ github.event.release.tag_name }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
@@ -77,13 +80,13 @@ jobs:
|
||||
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/dvc
|
||||
url: https://pypi.org/p/dvc/${{ github.event.release.tag_name }}
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
@@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main", "2.x"]
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
schedule:
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
@@ -18,35 +18,36 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04]
|
||||
pyv: ["3.10"]
|
||||
os: [ubuntu-latest]
|
||||
pyv: ["3.13"]
|
||||
plugin: ["dvc-s3"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
path: dvc
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
repository: iterative/${{ matrix.plugin }}
|
||||
ref: main
|
||||
path: ${{ matrix.plugin }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
dvc/pyproject.toml
|
||||
${{ matrix.plugin }}/setup.cfg
|
||||
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "**/pyproject.toml"
|
||||
cache-suffix: ${{ matrix.pyv }}
|
||||
|
||||
- name: Install plugin + DVC@PR
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install "./dvc[testing]"
|
||||
pip install -e "./${{ matrix.plugin }}[tests]"
|
||||
uv pip install "./dvc[testing]" --system
|
||||
uv pip install -e "./${{ matrix.plugin }}[tests]" --system
|
||||
|
||||
- name: Run plugin tests
|
||||
timeout-minutes: 15
|
||||
|
||||
+113
-21
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, 2.x]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '5 1 * * *' # every day at 01:05
|
||||
@@ -16,42 +16,134 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
timeout-minutes: 10
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
DVC_NO_ANALYTICS: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.13'
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: pyproject.toml
|
||||
- run: uv pip install -e ".[dev]" --system
|
||||
env:
|
||||
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||
|
||||
- name: Cache mypy
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: mypy-${{ runner.os }}-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}
|
||||
- uses: pre-commit/[email protected]
|
||||
|
||||
tests:
|
||||
timeout-minutes: 50
|
||||
timeout-minutes: 20
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
pyv: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
include:
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.9"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
- os: windows-latest
|
||||
pyv: "3.10"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.10"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.10"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.10"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
- os: windows-latest
|
||||
pyv: "3.11"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.11"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.11"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.11"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.12"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
- os: windows-latest
|
||||
pyv: "3.13"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 1"
|
||||
- os: windows-latest
|
||||
pyv: "3.13"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 2"
|
||||
- os: windows-latest
|
||||
pyv: "3.13"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 3"
|
||||
- os: windows-latest
|
||||
pyv: "3.13"
|
||||
pytestargs: "--splitting-algorithm=least_duration --splits 4 --group 4"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.pyv }}
|
||||
cache: pip
|
||||
cache-dependency-path: pyproject.toml
|
||||
- name: install
|
||||
run: |
|
||||
pip install --upgrade pip wheel
|
||||
pip install -e ".[dev]"
|
||||
- uses: pre-commit/[email protected]
|
||||
- uses: astral-sh/setup-uv@v6
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: pyproject.toml
|
||||
cache-suffix: ${{ matrix.pyv }}
|
||||
- run: uv pip install -e ".[dev]" pytest-split --system
|
||||
env:
|
||||
UV_LINK_MODE: ${{ runner.os == 'Windows' && 'symlink' || 'hardlink' }}
|
||||
|
||||
- name: run tests
|
||||
timeout-minutes: 40
|
||||
env:
|
||||
COVERAGE_CORE: sysmon
|
||||
PYTHONUTF8: 1
|
||||
run: >
|
||||
pytest -n=logical --timeout=300 --durations=100
|
||||
--cov --cov-report=xml --cov-report=term
|
||||
pytest ${{ matrix.pytestargs }} -n=logical --dist=worksteal --timeout=300 --durations=0
|
||||
--cov --cov-report=xml --cov-report=term --durations-path=./.github/.test_durations
|
||||
- name: upload coverage report
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
fail_ci_if_error: false
|
||||
uses: codecov/codecov-action@v5
|
||||
check:
|
||||
if: always()
|
||||
needs: [tests]
|
||||
@@ -66,7 +158,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2.2.1
|
||||
uses: rtCamp/action-slack-notify@v2.3.3
|
||||
env:
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ scripts/ci/gcp-creds.json
|
||||
.gcp-creds.json
|
||||
|
||||
*~
|
||||
/dvc/_dvc_version.py
|
||||
/dvc/_version.py
|
||||
|
||||
.mypy_cache/
|
||||
.pytest_cache/
|
||||
|
||||
@@ -6,7 +6,7 @@ ci:
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
@@ -22,13 +22,13 @@ repos:
|
||||
- id: sort-simple-yaml
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.2.2'
|
||||
rev: 'v0.13.2'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
rev: v2.4.1
|
||||
hooks:
|
||||
- id: codespell
|
||||
additional_dependencies: ["tomli"]
|
||||
@@ -50,7 +50,7 @@ repos:
|
||||
language: system
|
||||
name: DVC pre-commit
|
||||
stages:
|
||||
- commit
|
||||
- pre-commit
|
||||
verbose: true
|
||||
require_serial: true
|
||||
- args:
|
||||
@@ -61,7 +61,7 @@ repos:
|
||||
language: system
|
||||
name: DVC pre-push
|
||||
stages:
|
||||
- push
|
||||
- pre-push
|
||||
require_serial: true
|
||||
- always_run: true
|
||||
args:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
name: DVC pre-commit
|
||||
require_serial: true
|
||||
stages:
|
||||
- commit
|
||||
- pre-commit
|
||||
verbose: true
|
||||
- args:
|
||||
- git-hook
|
||||
@@ -20,7 +20,7 @@
|
||||
name: DVC pre-push
|
||||
require_serial: true
|
||||
stages:
|
||||
- push
|
||||
- pre-push
|
||||
- always_run: true
|
||||
args:
|
||||
- git-hook
|
||||
@@ -29,7 +29,7 @@
|
||||
id: dvc-post-checkout
|
||||
language: python
|
||||
language_version: python3
|
||||
minimum_pre_commit_version: 2.2.0
|
||||
minimum_pre_commit_version: 3.2.0
|
||||
name: DVC post-checkout
|
||||
require_serial: true
|
||||
stages:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
cff-version: 1.2.0
|
||||
title: 'DVC: Data Version Control - Git for Data & Models"'
|
||||
message: >-
|
||||
If you use this software as part of a publication and wish
|
||||
to cite it, please use the metadata from this file.
|
||||
type: software
|
||||
authors:
|
||||
- name: The DVC team and contributors
|
||||
website: 'https://github.com/iterative'
|
||||
repository-code: 'https://github.com/iterative/dvc'
|
||||
url: 'https://dvc.org'
|
||||
abstract: >-
|
||||
DVC is a tool for data science that takes advantage of existing software engineering toolset.
|
||||
It helps machine learning teams manage large datasets, make projects reproducible, and collaborate better.
|
||||
keywords:
|
||||
- ai
|
||||
- collaboration
|
||||
- data-science
|
||||
- data-version-control
|
||||
- developer-tools
|
||||
- git
|
||||
- machine-learning
|
||||
- python
|
||||
- reproducibility
|
||||
license: Apache-2.0
|
||||
+5
-5
@@ -213,7 +213,7 @@ Community and Support
|
||||
* `Forum <https://discuss.dvc.org/>`_
|
||||
* `Discord Chat <https://dvc.org/chat>`_
|
||||
* `Email <mailto:[email protected]>`_
|
||||
* `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
|
||||
* `Mailing List <https://dvc.org/community#subscribe>`_
|
||||
|
||||
Copyright
|
||||
=========
|
||||
@@ -240,11 +240,11 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
.. |VS Code Extension Overview| image:: https://raw.githubusercontent.com/iterative/vscode-dvc/main/extension/docs/overview.gif
|
||||
:alt: DVC Extension for VS Code
|
||||
|
||||
.. |CI| image:: https://github.com/iterative/dvc/workflows/Tests/badge.svg?branch=main
|
||||
:target: https://github.com/iterative/dvc/actions
|
||||
.. |CI| image:: https://github.com/iterative/dvc/actions/workflows/tests.yaml/badge.svg
|
||||
:target: https://github.com/iterative/dvc/actions/workflows/tests.yaml
|
||||
:alt: GHA Tests
|
||||
|
||||
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
|
||||
.. |Maintainability| image:: https://img.shields.io/codeclimate/maintainability/iterative/dvc
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
@@ -268,7 +268,7 @@ Barrak, A., Eghan, E.E. and Adams, B. `On the Co-evolution of ML Pipelines and S
|
||||
:target: https://formulae.brew.sh/formula/dvc
|
||||
:alt: Homebrew
|
||||
|
||||
.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/dvc.svg?label=conda&logo=conda-forge
|
||||
.. |Conda| image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
|
||||
:target: https://anaconda.org/conda-forge/dvc
|
||||
:alt: Conda-forge
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ DVC
|
||||
----
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
|
||||
import dvc.logger
|
||||
from dvc.build import PKG # noqa: F401
|
||||
from dvc.version import __version__, version_tuple # noqa: F401
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Main entry point for DVC command line tool."""
|
||||
|
||||
import sys
|
||||
|
||||
from dvc.cli import main
|
||||
|
||||
+5
-5
@@ -14,7 +14,7 @@ def viztracer_profile(
|
||||
log_async: bool = False,
|
||||
):
|
||||
try:
|
||||
import viztracer
|
||||
import viztracer # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
print("Failed to run profiler, viztracer is not installed") # noqa: T201
|
||||
yield
|
||||
@@ -37,7 +37,7 @@ def yappi_profile(
|
||||
separate_threads: Optional[bool] = False,
|
||||
):
|
||||
try:
|
||||
import yappi
|
||||
import yappi # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
print("Failed to run profiler, yappi is not installed") # noqa: T201
|
||||
yield
|
||||
@@ -79,7 +79,7 @@ def yappi_profile(
|
||||
def instrument(html_output=False):
|
||||
"""Run a statistical profiler"""
|
||||
try:
|
||||
from pyinstrument import Profiler
|
||||
from pyinstrument import Profiler # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
print("Failed to run profiler, pyinstrument is not installed") # noqa: T201
|
||||
yield
|
||||
@@ -123,7 +123,7 @@ def debug():
|
||||
yield
|
||||
except Exception:
|
||||
try:
|
||||
import ipdb as pdb # noqa: T100
|
||||
import ipdb as pdb # noqa: T100 # ty: ignore[unresolved-import]
|
||||
except ImportError:
|
||||
import pdb # type: ignore[no-redef] # noqa: T100
|
||||
pdb.post_mortem()
|
||||
@@ -162,7 +162,7 @@ def _get_path_func(tool: str, ext: str):
|
||||
fmt = f"{tool}.dvc-{{now:%Y%m%d}}_{{now:%H%M%S}}.{ext}"
|
||||
|
||||
def func(now: Optional["datetime"] = None) -> str:
|
||||
return fmt.format(now=now or datetime.now())
|
||||
return fmt.format(now=now or datetime.now()) # noqa: DTZ005
|
||||
|
||||
return func
|
||||
|
||||
|
||||
+73
-13
@@ -1,10 +1,14 @@
|
||||
import json
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from dvc.log import logger
|
||||
|
||||
from .env import DVC_ANALYTICS_ENDPOINT, DVC_NO_ANALYTICS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.scm import Base
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
@@ -67,6 +71,7 @@ def send(path):
|
||||
thus, the need of removing such file afterwards.
|
||||
"""
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
url = os.environ.get(DVC_ANALYTICS_ENDPOINT, "https://analytics.dvc.org")
|
||||
headers = {"content-type": "application/json"}
|
||||
@@ -81,7 +86,7 @@ def send(path):
|
||||
|
||||
try:
|
||||
requests.post(url, json=report, headers=headers, timeout=5)
|
||||
except requests.exceptions.RequestException as e:
|
||||
except RequestException as e:
|
||||
logger.trace("", exc_info=True)
|
||||
logger.debug("failed to send analytics report %s", str(e))
|
||||
|
||||
@@ -89,20 +94,62 @@ def send(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def _scm_in_use():
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import NoSCM
|
||||
def _git_remote_url(scm: Optional["Base"]) -> Optional[str]:
|
||||
from dvc.scm import Git
|
||||
|
||||
from .scm import SCM, SCMError
|
||||
if not isinstance(scm, Git):
|
||||
return None
|
||||
|
||||
from dulwich.porcelain import get_remote_repo
|
||||
|
||||
dulwich_repo = scm.dulwich.repo
|
||||
try:
|
||||
_remote, url = get_remote_repo(dulwich_repo)
|
||||
except IndexError:
|
||||
# IndexError happens when the head is detached
|
||||
_remote, url = get_remote_repo(dulwich_repo, b"origin")
|
||||
# Dulwich returns (None, "origin") if no remote set
|
||||
if (_remote, url) == (None, "origin"):
|
||||
return None
|
||||
return url
|
||||
|
||||
|
||||
def _scm_in_use(scm: Optional["Base"]) -> Optional[str]:
|
||||
return type(scm).__name__ if scm else None
|
||||
|
||||
|
||||
def _parse_git_remote_path(remote_url: str) -> str:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from scmrepo.urls import is_scp_style_url
|
||||
|
||||
parsed = urlparse(remote_url)
|
||||
# Windows Path also gets parsed with a drive letter as scheme
|
||||
# https://github.com/python/cpython/issues/86381
|
||||
if parsed.scheme and parsed.scheme in ("http", "https", "git", "ssh"):
|
||||
return parsed.path.strip("/")
|
||||
|
||||
if is_scp_style_url(remote_url):
|
||||
# handle scp-style URL
|
||||
parts = remote_url.split(":", 1)
|
||||
if len(parts) == 2:
|
||||
_, path = parts
|
||||
return path.rstrip("/")
|
||||
return remote_url
|
||||
|
||||
|
||||
def _git_remote_path_hash(scm: Optional["Base"]) -> Optional[str]:
|
||||
"""Return a hash of the git remote path."""
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
scm = SCM(root_dir=Repo.find_root())
|
||||
return type(scm).__name__
|
||||
except SCMError:
|
||||
return NoSCM.__name__
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
if remote_url := _git_remote_url(scm):
|
||||
path = _parse_git_remote_path(remote_url)
|
||||
h = hashlib.md5(path.encode("utf-8"), usedforsecurity=False) # for FIPS
|
||||
return h.hexdigest()
|
||||
except Exception: # noqa: BLE001
|
||||
logger.debug("Failed to get git remote path", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _runtime_info():
|
||||
@@ -112,6 +159,8 @@ def _runtime_info():
|
||||
from iterative_telemetry import _generate_ci_id, find_or_create_user_id
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.info import _get_remotes
|
||||
from dvc.repo import Repo
|
||||
from dvc.utils import is_binary
|
||||
|
||||
ci_id = _generate_ci_id()
|
||||
@@ -120,13 +169,24 @@ def _runtime_info():
|
||||
else:
|
||||
group_id, user_id = None, find_or_create_user_id()
|
||||
|
||||
scm = None
|
||||
remotes = None
|
||||
try:
|
||||
repo = Repo()
|
||||
scm = repo.scm
|
||||
remotes = _get_remotes(repo.config)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.debug("failed to open repo: %s", exc)
|
||||
|
||||
return {
|
||||
"dvc_version": __version__,
|
||||
"is_binary": is_binary(),
|
||||
"scm_class": _scm_in_use(),
|
||||
"scm_class": _scm_in_use(scm),
|
||||
"system_info": _system_info(),
|
||||
"user_id": user_id,
|
||||
"group_id": group_id,
|
||||
"remotes": remotes,
|
||||
"git_remote_hash": _git_remote_path_hash(scm),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
from dvc.fs.dvc import _DVCFileSystem as DVCFileSystem
|
||||
|
||||
from .artifacts import artifacts_show
|
||||
from .data import get_url, open, read
|
||||
from .data import get_url, open, read # noqa: A004
|
||||
from .dataset import get as get_dataset
|
||||
from .experiments import exp_save, exp_show
|
||||
from .scm import all_branches, all_commits, all_tags
|
||||
|
||||
+13
-2
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
from dvc.repo import Repo
|
||||
@@ -36,12 +37,22 @@ def artifacts_show(
|
||||
if version and stage:
|
||||
raise ValueError("Artifact version and stage are mutually exclusive.")
|
||||
|
||||
from dvc.repo.artifacts import Artifacts
|
||||
from dvc.utils import as_posix
|
||||
|
||||
repo_kwargs: dict[str, Any] = {
|
||||
"subrepos": True,
|
||||
"uninitialized": True,
|
||||
}
|
||||
|
||||
dirname, _ = Artifacts.parse_path(name)
|
||||
with Repo.open(repo, **repo_kwargs) as _repo:
|
||||
rev = _repo.artifacts.get_rev(name, version=version, stage=stage)
|
||||
with _repo.switch(rev):
|
||||
path = _repo.artifacts.get_path(name)
|
||||
return {"rev": rev, "path": path}
|
||||
root = _repo.fs.root_marker
|
||||
_dirname = _repo.fs.join(root, dirname) if dirname else root
|
||||
with Repo(_dirname, fs=_repo.fs, scm=_repo.scm) as r:
|
||||
path = r.artifacts.get_path(name)
|
||||
path = _repo.fs.join(_repo.fs.root_marker, as_posix(path))
|
||||
parts = _repo.fs.relparts(path, _repo.root_dir)
|
||||
return {"rev": rev, "path": os.path.join(*parts)}
|
||||
|
||||
+42
-6
@@ -25,7 +25,14 @@ def _wrap_exceptions(repo, url):
|
||||
raise PathMissingError(exc.path, url) from exc
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
def get_url(
|
||||
path: str,
|
||||
repo: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
remote: Optional[str] = None,
|
||||
config: Optional[dict[str, Any]] = None,
|
||||
remote_config: Optional[dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||
@@ -35,15 +42,44 @@ def get_url(path, repo=None, rev=None, remote=None):
|
||||
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
|
||||
Args:
|
||||
path (str): location and file name of the target, relative to the root
|
||||
of `repo`.
|
||||
repo (str, optional): location of the DVC project or Git Repo.
|
||||
Defaults to the current DVC 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): Any `Git revision`_ such as a branch or tag name,
|
||||
a commit hash or a dvc experiment name.
|
||||
Defaults to HEAD.
|
||||
If `repo` is not a Git repo, this option is ignored.
|
||||
remote (str, optional): Name of the `DVC remote`_ used to form the
|
||||
returned URL string.
|
||||
Defaults to the `default remote`_ of `repo`.
|
||||
For local projects, the cache is tried before the default remote.
|
||||
config(dict, optional): config to be passed to the DVC repository.
|
||||
Defaults to None.
|
||||
remote_config(dict, optional): remote config to be passed to the DVC
|
||||
repository.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: URL to the file or directory.
|
||||
"""
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc_data.index import StorageKeyError
|
||||
|
||||
repo_kwargs: dict[str, Any] = {}
|
||||
if remote:
|
||||
repo_kwargs["config"] = {"core": {"remote": remote}}
|
||||
with Repo.open(
|
||||
repo, rev=rev, subrepos=True, uninitialized=True, **repo_kwargs
|
||||
repo,
|
||||
rev=rev,
|
||||
subrepos=True,
|
||||
uninitialized=True,
|
||||
remote=remote,
|
||||
config=config,
|
||||
remote_config=remote_config,
|
||||
) as _repo:
|
||||
index, entry = _repo.get_data_index_entry(path)
|
||||
with reraise(
|
||||
@@ -249,7 +285,7 @@ def _open(
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
fs: Union["FileSystem", DataFileSystem, DVCFileSystem]
|
||||
fs: Union[FileSystem, DataFileSystem, DVCFileSystem]
|
||||
if os.path.isabs(path):
|
||||
fs = DataFileSystem(index=_repo.index.data["local"])
|
||||
fs_path = path
|
||||
|
||||
+7
-6
@@ -1,8 +1,8 @@
|
||||
from typing import Literal, TypedDict, Union
|
||||
|
||||
|
||||
class DVCXDataset(TypedDict):
|
||||
type: Literal["dvcx"]
|
||||
class DatachainDataset(TypedDict):
|
||||
type: Literal["dc"]
|
||||
name: str
|
||||
version: int
|
||||
|
||||
@@ -20,7 +20,7 @@ class URLDataset(TypedDict):
|
||||
path: str
|
||||
|
||||
|
||||
def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
|
||||
def get(name: str) -> Union[DatachainDataset, DVCDataset, URLDataset]:
|
||||
from difflib import get_close_matches
|
||||
|
||||
from dvc.fs import get_cloud_fs
|
||||
@@ -46,9 +46,9 @@ def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
|
||||
path=dataset.lock.path,
|
||||
sha=dataset.lock.rev_lock,
|
||||
)
|
||||
if dataset.type == "dvcx":
|
||||
return DVCXDataset(
|
||||
type="dvcx", name=dataset.name_version[0], version=dataset.lock.version
|
||||
if dataset.type == "dc":
|
||||
return DatachainDataset(
|
||||
type="dc", name=dataset.name_version[0], version=dataset.lock.version
|
||||
)
|
||||
if dataset.type == "url":
|
||||
fs_cls, _, path = get_cloud_fs(repo.config, url=dataset.lock.url)
|
||||
@@ -64,3 +64,4 @@ def get(name: str) -> Union[DVCXDataset, DVCDataset, URLDataset]:
|
||||
for file in dataset.lock.files
|
||||
]
|
||||
return URLDataset(type="url", files=files, path=versioned_path)
|
||||
raise AssertionError("unreachable")
|
||||
|
||||
+1
-2
@@ -27,8 +27,7 @@ def _postprocess(results):
|
||||
}
|
||||
processed[rev] = processed[rev] | to_merge
|
||||
|
||||
if "workspace" in processed:
|
||||
del processed["workspace"]
|
||||
processed.pop("workspace", None)
|
||||
|
||||
return processed
|
||||
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ class CacheManager:
|
||||
return FileStorage(
|
||||
key=(),
|
||||
fs=self.local.fs,
|
||||
path=self.local.fs.join(self.default_local_cache_dir, self.FS_DIR),
|
||||
path=self.local.fs.join(self.local_cache_dir, self.FS_DIR),
|
||||
)
|
||||
|
||||
def _init_odb(self, schemes):
|
||||
|
||||
+6
-5
@@ -157,6 +157,8 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
Returns:
|
||||
int: command's return code.
|
||||
"""
|
||||
from contextlib import ExitStack
|
||||
|
||||
from dvc._debug import debugtools
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
@@ -173,12 +175,11 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
logging.disable(logging.INFO)
|
||||
|
||||
args = None
|
||||
|
||||
outer_log_level = logger.level
|
||||
level = None
|
||||
stack = ExitStack()
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
level = None
|
||||
if args.quiet:
|
||||
level = logging.CRITICAL
|
||||
elif args.verbose == 1:
|
||||
@@ -187,7 +188,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
level = logging.TRACE # type: ignore[attr-defined]
|
||||
|
||||
if level is not None:
|
||||
set_loggers_level(level)
|
||||
stack.enter_context(set_loggers_level(level))
|
||||
|
||||
if level and level <= logging.DEBUG:
|
||||
from platform import platform, python_implementation, python_version
|
||||
@@ -247,7 +248,7 @@ def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
||||
|
||||
return ret
|
||||
finally:
|
||||
logger.setLevel(outer_log_level)
|
||||
stack.close()
|
||||
|
||||
from dvc.repo.open_repo import clean_repos
|
||||
|
||||
|
||||
+5
-2
@@ -18,8 +18,11 @@ class CmdBase(ABC):
|
||||
|
||||
os.chdir(args.cd)
|
||||
|
||||
self.repo: "Repo" = Repo(uninitialized=self.UNINITIALIZED)
|
||||
self.config: "Config" = self.repo.config
|
||||
self.repo: Repo = Repo(
|
||||
uninitialized=self.UNINITIALIZED,
|
||||
_wait_for_lock=args.wait_for_lock,
|
||||
)
|
||||
self.config: Config = self.repo.config
|
||||
self.args = args
|
||||
|
||||
def do_run(self):
|
||||
|
||||
+163
-3
@@ -22,6 +22,36 @@ _dvc_compgen_exps() {
|
||||
local _dvc_exps=($(dvc exp list -q --all-commits --names-only))
|
||||
compgen -W "${_dvc_exps[*]}" -- $1
|
||||
}
|
||||
|
||||
_dvc_compgen_remotes() {
|
||||
local _dvc_remotes=($(dvc remote list | cut -d' ' -f1))
|
||||
compgen -W "${_dvc_remotes[*]}" -- $1
|
||||
}
|
||||
|
||||
_dvc_compgen_config_vars() {
|
||||
compgen -W "${_dvc_config_vars[*]}" -- $1
|
||||
}
|
||||
|
||||
_dvc_compgen_remote_config_vars() {
|
||||
local cur prev words cword remote_name filtered
|
||||
_init_completion || return
|
||||
|
||||
# last non-option word before cursor
|
||||
local nonopts=()
|
||||
for w in "${words[@]:1:$((cword-1))}"; do
|
||||
[[ $w == -* ]] || nonopts+=("$w")
|
||||
done
|
||||
|
||||
remote_name="${nonopts[-1]}"
|
||||
if [[ -n $remote_name ]]; then
|
||||
local _dvc_remote_config_vars=($(
|
||||
dvc config --available-options -q 2> /dev/null \
|
||||
| grep "^remote\\.${remote_name}\\." \
|
||||
| sed "s/^remote\\.${remote_name}\\.//"
|
||||
))
|
||||
compgen -W "${_dvc_remote_config_vars[*]}" -- $1
|
||||
fi
|
||||
}
|
||||
"""
|
||||
|
||||
ZSH_PREAMBLE = """
|
||||
@@ -55,19 +85,149 @@ _dvc_compadd_stages_and_files() {
|
||||
_dvc_compadd_exps() {
|
||||
_describe 'experiments' "($(dvc exp list -q --all-commits --names-only))"
|
||||
}
|
||||
|
||||
_dvc_compadd_remotes() {
|
||||
_describe 'remotes' "($(dvc remote list | cut -d' ' -f1))"
|
||||
}
|
||||
|
||||
_dvc_config_config_vars_for_completion() {
|
||||
dvc config --available-options -q 2> /dev/null
|
||||
}
|
||||
|
||||
_dvc_compadd_config_vars() {
|
||||
_describe 'config_vars' _dvc_config_vars
|
||||
}
|
||||
|
||||
_dvc_compadd_remote_config_vars() {
|
||||
local remote_name filtered
|
||||
|
||||
# last non-option word before cursor
|
||||
remote_name=${${words[1,CURRENT-1]##-*}[-1]}
|
||||
if [[ -n $remote_name ]]; then
|
||||
filtered=$(_dvc_config_config_vars_for_completion \\
|
||||
| grep "^remote.${remote_name}\\\\." \\
|
||||
| sed "s/^remote\\\\.${remote_name}\\\\.//"
|
||||
)
|
||||
compadd -- $=filtered
|
||||
fi
|
||||
}
|
||||
"""
|
||||
|
||||
FISH_PREAMBLE = """
|
||||
function __fish_complete_dvc_files
|
||||
__fish_complete_path | string match -re '\\*?.dvc|Dvcfile|dvc\\.yaml'
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_stages
|
||||
for line in (dvc stage list -q)
|
||||
set -l parts (string split -m1 ' ' -- $line)
|
||||
set -l name $parts[1]
|
||||
set -l desc (string trim $parts[2])
|
||||
echo -e "$name $desc"
|
||||
end
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_stages_and_files
|
||||
__fish_complete_dvc_stages
|
||||
__fish_complete_dvc_files
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_experiments
|
||||
dvc exp list -q --all-commits --names-only
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_remotes
|
||||
dvc remote list | cut -d' ' -f1
|
||||
end
|
||||
|
||||
function __fish_complete_dvc_remote_config_vars
|
||||
# last non-option word before cursor
|
||||
set -l remote_name (commandline -opc | string match -rv '^-' | tail -n 1)
|
||||
|
||||
if test -n "$remote_name"
|
||||
dvc config --available-options -q 2>/dev/null |
|
||||
string match -re "^remote\\.$remote_name\\." |
|
||||
string replace -r "^remote\\.$remote_name\\." ''
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
PREAMBLE = {
|
||||
"bash": BASH_PREAMBLE,
|
||||
"zsh": ZSH_PREAMBLE,
|
||||
"fish": FISH_PREAMBLE,
|
||||
}
|
||||
|
||||
FILE = shtab.FILE
|
||||
DIR = shtab.DIRECTORY
|
||||
DVC_FILE = {"bash": "_dvc_compgen_DVCFiles", "zsh": "_dvc_compadd_DVCFiles"}
|
||||
STAGE = {"bash": "_dvc_compgen_stages", "zsh": "_dvc_compadd_stages"}
|
||||
DVC_FILE = {
|
||||
"bash": "_dvc_compgen_DVCFiles",
|
||||
"zsh": "_dvc_compadd_DVCFiles",
|
||||
"fish": '-f -a "(__fish_complete_dvc_files)"',
|
||||
}
|
||||
STAGE = {
|
||||
"bash": "_dvc_compgen_stages",
|
||||
"zsh": "_dvc_compadd_stages",
|
||||
"fish": '-f -a "(__fish_complete_dvc_stages)"',
|
||||
}
|
||||
DVCFILES_AND_STAGE = {
|
||||
"bash": "_dvc_compgen_stages_and_files",
|
||||
"zsh": "_dvc_compadd_stages_and_files",
|
||||
"fish": '-f -a "(__fish_complete_dvc_stages_and_files)"',
|
||||
}
|
||||
EXPERIMENT = {"bash": "_dvc_compgen_exps", "zsh": "_dvc_compadd_exps"}
|
||||
EXPERIMENT = {
|
||||
"bash": "_dvc_compgen_exps",
|
||||
"zsh": "_dvc_compadd_exps",
|
||||
"fish": '-f -a "(__fish_complete_dvc_experiments)"',
|
||||
}
|
||||
REMOTE = {
|
||||
"bash": "_dvc_compgen_remotes",
|
||||
"zsh": "_dvc_compadd_remotes",
|
||||
"fish": '-f -a "(__fish_complete_dvc_remotes)"',
|
||||
}
|
||||
CONFIG_VARS = {
|
||||
"bash": "_dvc_compgen_config_vars",
|
||||
"zsh": "_dvc_compadd_config_vars",
|
||||
"fish": '-f -a "(__fish_complete_dvc_config_vars)"',
|
||||
}
|
||||
REMOTE_CONFIG_VARS = {
|
||||
"bash": "_dvc_compgen_remote_config_vars",
|
||||
"zsh": "_dvc_compadd_remote_config_vars",
|
||||
"fish": '-f -a "(__fish_complete_dvc_remote_config_vars)"',
|
||||
}
|
||||
|
||||
|
||||
def get_preamble() -> dict[str, str]:
|
||||
from dvc.config_schema import config_vars_for_completion
|
||||
|
||||
ret: dict[str, str] = {}
|
||||
config_vars = list(config_vars_for_completion())
|
||||
|
||||
nl = "\n\t".expandtabs(4)
|
||||
config_vars_arr = f"""
|
||||
_dvc_config_vars=(
|
||||
{nl.join(config_vars)}
|
||||
)
|
||||
"""
|
||||
indent = "\t\t".expandtabs(4) # 8 spaces
|
||||
lines = (
|
||||
"\n".join(
|
||||
f"{indent}{c} \\"
|
||||
for c in config_vars[:-1] # all but last
|
||||
)
|
||||
+ "\n"
|
||||
+ f"{indent}{config_vars[-1]}"
|
||||
) # last line without backslash
|
||||
config_vars_arr_fish = f"""
|
||||
function __fish_complete_dvc_config_vars
|
||||
set -l _dvc_config_vars \\
|
||||
{lines}
|
||||
printf %s\\n $_dvc_config_vars
|
||||
end
|
||||
"""
|
||||
for shell, preamble in PREAMBLE.items():
|
||||
if shell != "fish":
|
||||
ret[shell] = config_vars_arr + preamble
|
||||
else:
|
||||
ret[shell] = config_vars_arr_fish + preamble
|
||||
return ret
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Main parser for the dvc cli."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from functools import lru_cache
|
||||
@@ -199,6 +200,14 @@ def get_main_parser():
|
||||
type=str,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"--wait-for-lock",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Wait for the lock if it is already held by another process, instead of"
|
||||
" failing immediately.",
|
||||
)
|
||||
|
||||
# Sub commands
|
||||
subparsers = parser.add_subparsers(
|
||||
title="Available Commands",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ def hide_subparsers_from_help(subparsers):
|
||||
# from the 'positional arguments' choices list
|
||||
# see: https://bugs.python.org/issue22848
|
||||
# Need to set `add_help=False`, but avoid setting `help`
|
||||
# (not even to `argparse.SUPPPRESS`).
|
||||
# (not even to `argparse.SUPPRESS`).
|
||||
# NOTE: The argument is the parent subparser, not the subcommand parser.
|
||||
cmds = [cmd for cmd, parser in subparsers.choices.items() if parser.add_help]
|
||||
subparsers.metavar = "{{{}}}".format(",".join(cmds))
|
||||
|
||||
+9
-1
@@ -51,6 +51,7 @@ class CmdAdd(CmdBase):
|
||||
to_remote=self.args.to_remote,
|
||||
remote_jobs=self.args.remote_jobs,
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
@@ -100,7 +101,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to download to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
parser.add_argument(
|
||||
"--remote-jobs",
|
||||
type=int,
|
||||
@@ -119,6 +120,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Override local file or folder if exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-relink",
|
||||
dest="relink",
|
||||
action="store_false",
|
||||
help="Don't recreate links from cache to workspace.",
|
||||
)
|
||||
parser.set_defaults(relink=True)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
).complete = completion.FILE
|
||||
|
||||
@@ -13,6 +13,9 @@ class CmdArtifactsGet(CmdBaseNoRepo):
|
||||
from dvc.scm import CloneError
|
||||
from dvc.ui import ui
|
||||
|
||||
if self.args.show_url:
|
||||
return self._show_url()
|
||||
|
||||
try:
|
||||
count, out = Artifacts.get(
|
||||
self.args.url,
|
||||
@@ -36,6 +39,28 @@ class CmdArtifactsGet(CmdBaseNoRepo):
|
||||
)
|
||||
return 1
|
||||
|
||||
def _show_url(self):
|
||||
from dvc.api import artifacts_show, get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
artifact = artifacts_show(
|
||||
self.args.name,
|
||||
version=self.args.rev,
|
||||
stage=self.args.stage,
|
||||
repo=self.args.url,
|
||||
)
|
||||
|
||||
url = get_url(
|
||||
artifact["path"],
|
||||
repo=self.args.url,
|
||||
rev=artifact["rev"],
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
ARTIFACTS_HELP = "DVC model registry artifact commands."
|
||||
@@ -84,6 +109,14 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Destination path to download artifact to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
get_parser.add_argument(
|
||||
"--show-url",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Print the storage location (URL) the target data would be "
|
||||
"downloaded from, and exit."
|
||||
),
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
@@ -116,7 +149,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"Remote name to set as a default in the target repository "
|
||||
"(only applicable when downloading from DVC remote)."
|
||||
),
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
get_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.ui import ui
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.ignore import CheckIgnoreResult
|
||||
|
||||
|
||||
class CmdCheckIgnore(CmdBase):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
self.ignore_filter = self.repo.dvcignore
|
||||
|
||||
def _show_results(self, result):
|
||||
if not result.match and not self.args.non_matching:
|
||||
return
|
||||
def _show_results(self, result: "CheckIgnoreResult"):
|
||||
if not result.match and not self.args.details:
|
||||
return None
|
||||
|
||||
if self.args.details:
|
||||
patterns = result.patterns
|
||||
pattern_infos = result.pattern_infos
|
||||
patterns = [str(pi) for pi in pattern_infos]
|
||||
if not patterns and self.args.non_matching:
|
||||
patterns = ["::"]
|
||||
if not self.args.all:
|
||||
patterns = patterns[-1:]
|
||||
|
||||
@@ -22,13 +30,11 @@ class CmdCheckIgnore(CmdBase):
|
||||
ui.write(pattern, result.file, sep="\t")
|
||||
else:
|
||||
ui.write(result.file)
|
||||
return bool(result.pattern_infos)
|
||||
|
||||
def _check_one_file(self, target):
|
||||
result = self.ignore_filter.check_ignore(target)
|
||||
self._show_results(result)
|
||||
if result.match:
|
||||
return 0
|
||||
return 1
|
||||
return bool(self._show_results(result))
|
||||
|
||||
def _interactive_mode(self):
|
||||
ret = 1
|
||||
@@ -39,14 +45,14 @@ class CmdCheckIgnore(CmdBase):
|
||||
break
|
||||
if not target:
|
||||
break
|
||||
if not self._check_one_file(target):
|
||||
if self._check_one_file(target):
|
||||
ret = 0
|
||||
return ret
|
||||
|
||||
def _normal_mode(self):
|
||||
ret = 1
|
||||
for target in self.args.targets:
|
||||
if not self._check_one_file(target):
|
||||
if self._check_one_file(target):
|
||||
ret = 0
|
||||
return ret
|
||||
|
||||
@@ -76,6 +82,8 @@ class CmdCheckIgnore(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
import argparse
|
||||
|
||||
ADD_HELP = "Check whether files or directories are excluded due to `.dvcignore`."
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
@@ -93,14 +101,7 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Show the exclude patterns along with each target path.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--all",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Include the target paths which don't match any pattern "
|
||||
"in the `--details` list."
|
||||
),
|
||||
"-a", "--all", action="store_true", default=False, help=argparse.SUPPRESS
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import operator
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
@@ -30,7 +28,7 @@ class CmdCheckout(CmdBase):
|
||||
|
||||
stats, exc = None, None
|
||||
try:
|
||||
stats = self.repo.checkout(
|
||||
result = self.repo.checkout(
|
||||
targets=self.args.targets,
|
||||
with_deps=self.args.with_deps,
|
||||
force=self.args.force,
|
||||
@@ -40,14 +38,17 @@ class CmdCheckout(CmdBase):
|
||||
)
|
||||
except CheckoutError as _exc:
|
||||
exc = _exc
|
||||
stats = exc.stats
|
||||
result = exc.result
|
||||
|
||||
if self.args.summary:
|
||||
default_message = "No changes."
|
||||
msg = get_summary(sorted(stats.items(), key=operator.itemgetter(0)))
|
||||
stats = result["stats"]
|
||||
assert isinstance(stats, dict)
|
||||
msg = get_summary(stats.items())
|
||||
ui.write(msg or default_message)
|
||||
else:
|
||||
log_changes(stats)
|
||||
result.pop("stats", {})
|
||||
log_changes(result)
|
||||
|
||||
if exc:
|
||||
raise exc
|
||||
|
||||
@@ -20,6 +20,7 @@ class CmdCommit(CmdBase):
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to commit%s", (" " + target) if target else "")
|
||||
@@ -64,6 +65,13 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Commit cache for subdirectories of the specified directory.",
|
||||
)
|
||||
commit_parser.add_argument(
|
||||
"--no-relink",
|
||||
dest="relink",
|
||||
action="store_false",
|
||||
help="Don't recreate links from cache to workspace.",
|
||||
)
|
||||
commit_parser.set_defaults(relink=True)
|
||||
commit_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.completion import PREAMBLE
|
||||
from dvc.cli.completion import get_preamble
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
from dvc.ui import ui
|
||||
@@ -8,7 +8,7 @@ from dvc.ui import ui
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
SUPPORTED_SHELLS = ["bash", "zsh"]
|
||||
SUPPORTED_SHELLS = ["bash", "zsh", "fish"]
|
||||
|
||||
|
||||
class CmdCompletion(CmdBaseNoRepo):
|
||||
@@ -17,7 +17,7 @@ class CmdCompletion(CmdBaseNoRepo):
|
||||
|
||||
shell = self.args.shell
|
||||
parser = self.args.parser
|
||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
||||
script = shtab.complete(parser, shell=shell, preamble=get_preamble())
|
||||
ui.write(script, force=True)
|
||||
return 0
|
||||
|
||||
|
||||
+14
-2
@@ -3,7 +3,7 @@ import os
|
||||
|
||||
from funcy import set_in
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -40,6 +40,15 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
self.config = Config.from_cwd(validate=False)
|
||||
|
||||
def run(self):
|
||||
if self.args.available_options:
|
||||
from dvc.config_schema import contextual_config_vars_for_completion
|
||||
|
||||
ui.write(
|
||||
"\n".join(list(contextual_config_vars_for_completion(self.config))),
|
||||
force=True,
|
||||
)
|
||||
return 0
|
||||
|
||||
if self.args.show_origin and (self.args.value or self.args.unset):
|
||||
logger.error(
|
||||
"--show-origin can't be used together with any of these "
|
||||
@@ -212,6 +221,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=CONFIG_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"--available-options", default=False, action="store_true"
|
||||
)
|
||||
config_parser.add_argument(
|
||||
"-u",
|
||||
"--unset",
|
||||
@@ -224,7 +236,7 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
type=_name_type,
|
||||
help="Option name (section.option or remote.name.option).",
|
||||
)
|
||||
).complete = completion.CONFIG_VARS
|
||||
config_parser.add_argument("value", nargs="?", help="Option value.")
|
||||
config_parser.add_argument(
|
||||
"-l",
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ def _show_ascii(graph: "DiGraph"):
|
||||
|
||||
ret = []
|
||||
for pipeline in pipelines:
|
||||
ret.append(draw(pipeline.nodes, pipeline.edges))
|
||||
ret.append(draw(pipeline.nodes, pipeline.edges)) # noqa: PERF401
|
||||
|
||||
return "\n".join(ret)
|
||||
|
||||
|
||||
+36
-12
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from funcy import chunks, compact, log_durations
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -10,6 +10,7 @@ from dvc.ui import ui
|
||||
from dvc.utils import colorize
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.repo.data import GitInfo
|
||||
from dvc.repo.data import Status as DataStatus
|
||||
|
||||
|
||||
@@ -55,20 +56,15 @@ class CmdDataStatus(CmdBase):
|
||||
def _process_status(status: "DataStatus"):
|
||||
"""Flatten stage status, and filter empty stage status contents."""
|
||||
for stage, stage_status in status.items():
|
||||
items = stage_status
|
||||
if isinstance(stage_status, dict):
|
||||
items = {
|
||||
file: state
|
||||
for state, files in stage_status.items()
|
||||
for file in files
|
||||
}
|
||||
if not items:
|
||||
if not stage_status or (
|
||||
isinstance(stage_status, dict) and not any(stage_status.values())
|
||||
):
|
||||
continue
|
||||
yield stage, items
|
||||
yield stage, stage_status
|
||||
|
||||
@classmethod
|
||||
def _show_status(cls, status: "DataStatus") -> int: # noqa: C901
|
||||
git_info = status.pop("git") # type: ignore[misc]
|
||||
git_info: GitInfo = status.pop("git") # type: ignore[misc]
|
||||
result = dict(cls._process_status(status))
|
||||
if not result:
|
||||
no_changes = "No changes"
|
||||
@@ -90,7 +86,16 @@ class CmdDataStatus(CmdBase):
|
||||
ui.write(f" ({hint})")
|
||||
|
||||
if isinstance(stage_status, dict):
|
||||
items = [f"{state}: {file}" for file, state in stage_status.items()]
|
||||
items = [
|
||||
f"{state}: "
|
||||
+ (
|
||||
" -> ".join(change.values())
|
||||
if isinstance(change, dict)
|
||||
else change
|
||||
)
|
||||
for state, changes in stage_status.items()
|
||||
for change in changes
|
||||
]
|
||||
else:
|
||||
items = stage_status
|
||||
|
||||
@@ -108,8 +113,10 @@ class CmdDataStatus(CmdBase):
|
||||
def run(self) -> int:
|
||||
with log_durations(logger.trace, "in data_status"):
|
||||
status = self.repo.data_status(
|
||||
targets=self.args.targets,
|
||||
granular=self.args.granular,
|
||||
untracked_files=self.args.untracked_files,
|
||||
remote=self.args.remote,
|
||||
not_in_remote=self.args.not_in_remote,
|
||||
remote_refresh=self.args.remote_refresh,
|
||||
)
|
||||
@@ -126,9 +133,12 @@ class CmdDataStatus(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
data_help = "Commands related to data management."
|
||||
data_parser = subparsers.add_parser(
|
||||
"data",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(data_help, "data/status"),
|
||||
help=data_help,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
data_subparsers = data_parser.add_subparsers(
|
||||
@@ -147,6 +157,14 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
help=DATA_STATUS_HELP,
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files and stage names."
|
||||
),
|
||||
).complete = completion.FILE # type: ignore[attr-defined]
|
||||
data_status_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
@@ -173,6 +191,12 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="?",
|
||||
help="Show untracked files.",
|
||||
)
|
||||
data_status_parser.add_argument(
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to check (only applicable with --not-in-remote).",
|
||||
metavar="<name>",
|
||||
).complete = completion.REMOTE
|
||||
data_status_parser.add_argument(
|
||||
"--not-in-remote",
|
||||
action="store_true",
|
||||
|
||||
+14
-13
@@ -22,17 +22,18 @@ class CmdDataBase(CmdBase):
|
||||
|
||||
|
||||
class CmdDataPull(CmdDataBase):
|
||||
def log_summary(self, stats):
|
||||
def log_summary(self, result):
|
||||
from dvc.commands.checkout import log_changes
|
||||
|
||||
log_changes(stats)
|
||||
stats = result.pop("stats", {})
|
||||
log_changes(result)
|
||||
super().log_summary(stats)
|
||||
|
||||
def run(self):
|
||||
from dvc.exceptions import CheckoutError, DvcException
|
||||
|
||||
try:
|
||||
stats = self.repo.pull(
|
||||
result = self.repo.pull(
|
||||
targets=self.args.targets,
|
||||
jobs=self.args.jobs,
|
||||
remote=self.args.remote,
|
||||
@@ -46,10 +47,10 @@ class CmdDataPull(CmdDataBase):
|
||||
glob=self.args.glob,
|
||||
allow_missing=self.args.allow_missing,
|
||||
)
|
||||
self.log_summary(stats)
|
||||
self.log_summary(result)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
if stats := getattr(exc, "stats", {}):
|
||||
self.log_summary(stats)
|
||||
if result := getattr(exc, "result", {}):
|
||||
self.log_summary(result)
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
|
||||
@@ -149,7 +150,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to pull from", metavar="<name>"
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
pull_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -194,7 +195,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
@@ -224,7 +225,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to push to", metavar="<name>"
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
push_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -262,7 +263,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
@@ -286,7 +287,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to fetch from", metavar="<name>"
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
fetch_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -324,7 +325,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
@@ -379,7 +380,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to compare local cache to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
status_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
|
||||
@@ -34,7 +34,7 @@ class CmdDatasetAdd(CmdBase):
|
||||
|
||||
url = dataset.spec.url
|
||||
ver: str = ""
|
||||
if dataset.type == "dvcx":
|
||||
if dataset.type == "dc":
|
||||
ver = f"v{dataset.lock.version}"
|
||||
if dataset.type == "dvc":
|
||||
if dataset.lock.path:
|
||||
@@ -42,7 +42,7 @@ class CmdDatasetAdd(CmdBase):
|
||||
if rev := dataset.lock.rev:
|
||||
ver = rev
|
||||
|
||||
ver_part: Optional["Text"] = None
|
||||
ver_part: Optional[Text] = None
|
||||
if ver:
|
||||
ver_part = ui.rich_text.assemble(" @ ", (ver, "repr.number"))
|
||||
text = ui.rich_text.assemble("(", (url, "repr.url"), ver_part or "", ")")
|
||||
@@ -55,7 +55,7 @@ class CmdDatasetAdd(CmdBase):
|
||||
raise DvcException("--path can't be used without --dvc")
|
||||
|
||||
d = vars(self.args)
|
||||
for key in ["dvc", "dvcx", "url"]:
|
||||
for key in ["dvc", "dc", "url"]:
|
||||
if url := d.pop(key, None):
|
||||
d.update({"type": key, "url": url})
|
||||
break
|
||||
@@ -83,13 +83,13 @@ class CmdDatasetUpdate(CmdBase):
|
||||
return CmdDatasetAdd.display(name, new, action)
|
||||
if dataset == new:
|
||||
ui.write("[yellow]Nothing to update[/]", styled=True)
|
||||
return
|
||||
return None
|
||||
|
||||
assert new.lock
|
||||
|
||||
v: Optional[tuple[str, str]] = None
|
||||
if dataset.type == "dvcx":
|
||||
assert new.type == "dvcx"
|
||||
if dataset.type == "dc":
|
||||
assert new.type == "dc"
|
||||
if new.lock.version < dataset.lock.version:
|
||||
action = "Downgrading"
|
||||
|
||||
@@ -112,6 +112,7 @@ class CmdDatasetUpdate(CmdBase):
|
||||
assert new.type == "url"
|
||||
stats = diff_files(dataset.lock.files, new.lock.files)
|
||||
log_changes(stats)
|
||||
return None
|
||||
|
||||
def run(self):
|
||||
from difflib import get_close_matches
|
||||
@@ -168,7 +169,7 @@ def add_parser(subparsers, parent_parser):
|
||||
|
||||
url_exclusive_group = ds_add_parser.add_mutually_exclusive_group(required=True)
|
||||
url_exclusive_group.add_argument(
|
||||
"--dvcx", metavar="name", help="Name of the dvcx dataset to track"
|
||||
"--dc", metavar="name", help="Name of the DataChain dataset to track"
|
||||
)
|
||||
url_exclusive_group.add_argument(
|
||||
"--dvc",
|
||||
@@ -219,7 +220,7 @@ remote://remote_name/path/to/file/or/dir (see `dvc remote`)
|
||||
"--rev",
|
||||
"--version",
|
||||
nargs="?",
|
||||
help="DVCX dataset version or Git revision (e.g. SHA, branch, tag)",
|
||||
help="DataChain dataset version or Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<version>",
|
||||
)
|
||||
ds_update_parser.set_defaults(func=CmdDatasetUpdate)
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
du_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
@@ -58,6 +58,15 @@ def add_parser(subparsers, parent_parser):
|
||||
hide_subparsers_from_help(experiments_subparsers)
|
||||
|
||||
|
||||
def add_keep_selection_flag(experiments_subcmd_parser):
|
||||
experiments_subcmd_parser.add_argument(
|
||||
"--keep",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep the selected experiments instead of removing them.",
|
||||
)
|
||||
|
||||
|
||||
def add_rev_selection_flags(
|
||||
experiments_subcmd_parser, command: str, default: bool = True
|
||||
):
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from dvc.cli import formatter
|
||||
import argparse
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -69,7 +71,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
dest="dvc_remote",
|
||||
metavar="<name>",
|
||||
help="Name of the DVC remote to use when pulling cached outputs.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
experiments_pull_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
@@ -81,7 +83,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Pull run history for all stages.",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import argparse
|
||||
from typing import Any
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
@@ -110,7 +111,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
dest="dvc_remote",
|
||||
metavar="<name>",
|
||||
help="Name of the DVC remote to use when pushing cached outputs.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
experiments_push_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
@@ -120,7 +121,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
|
||||
@@ -34,6 +34,7 @@ class CmdExperimentsRemove(CmdBase):
|
||||
num=self.args.num,
|
||||
queue=self.args.queue,
|
||||
git_remote=self.args.git_remote,
|
||||
keep=self.args.keep,
|
||||
)
|
||||
if removed:
|
||||
ui.write(f"Removed experiments: {humanize.join(map(repr, removed))}")
|
||||
@@ -44,7 +45,7 @@ class CmdExperimentsRemove(CmdBase):
|
||||
|
||||
|
||||
def add_parser(experiments_subparsers, parent_parser):
|
||||
from . import add_rev_selection_flags
|
||||
from . import add_keep_selection_flag, add_rev_selection_flags
|
||||
|
||||
EXPERIMENTS_REMOVE_HELP = "Remove experiments."
|
||||
experiments_remove_parser = experiments_subparsers.add_parser(
|
||||
@@ -57,6 +58,7 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
remove_group = experiments_remove_parser.add_mutually_exclusive_group()
|
||||
add_rev_selection_flags(experiments_remove_parser, "Remove", False)
|
||||
add_keep_selection_flag(experiments_remove_parser)
|
||||
remove_group.add_argument(
|
||||
"--queue", action="store_true", help="Remove all queued experiments."
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import argparse
|
||||
import re
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Collection, Iterable
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -59,17 +59,16 @@ def baseline_styler(typ):
|
||||
def show_experiments(
|
||||
td: "TabularData",
|
||||
headers: dict[str, Iterable[str]],
|
||||
keep=None,
|
||||
drop=None,
|
||||
keep: Collection[str] = (),
|
||||
drop: Collection[str] = (),
|
||||
pager=True,
|
||||
csv=False,
|
||||
markdown=False,
|
||||
**kwargs,
|
||||
):
|
||||
if keep:
|
||||
for col in td.keys(): # noqa: SIM118
|
||||
if re.match(keep, col):
|
||||
td.protect(col)
|
||||
keep_re = re.compile("|".join(keep))
|
||||
td.protect(*(col for col in td.keys() if keep_re.match(col))) # noqa: SIM118
|
||||
|
||||
for col in ("State", "Executor"):
|
||||
if td.is_empty(col):
|
||||
@@ -110,8 +109,9 @@ def show_experiments(
|
||||
td.drop_duplicates("cols", ignore_empty=False)
|
||||
|
||||
cols_to_drop = set()
|
||||
if drop is not None:
|
||||
cols_to_drop = {col for col in td.keys() if re.match(drop, col)} # noqa: SIM118
|
||||
if drop:
|
||||
drop_re = re.compile("|".join(drop))
|
||||
cols_to_drop = {col for col in td.keys() if drop_re.match(col)} # noqa: SIM118
|
||||
td.drop(*cols_to_drop)
|
||||
|
||||
td.render(
|
||||
@@ -150,6 +150,7 @@ class CmdExperimentsShow(CmdBase):
|
||||
all_commits=self.args.all_commits,
|
||||
hide_queued=self.args.hide_queued,
|
||||
hide_failed=self.args.hide_failed,
|
||||
hide_workspace=self.args.hide_workspace,
|
||||
revs=self.args.rev,
|
||||
num=self.args.num,
|
||||
sha_only=self.args.sha,
|
||||
@@ -236,11 +237,15 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--drop",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Remove the columns matching the specified regex pattern.",
|
||||
metavar="<regex_pattern>",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--keep",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Preserve the columns matching the specified regex pattern.",
|
||||
metavar="<regex_pattern>",
|
||||
)
|
||||
@@ -279,6 +284,12 @@ def add_parser(experiments_subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Hide queued experiments in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--hide-workspace",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Hide workspace row in the table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBase
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.log import logger
|
||||
@@ -54,13 +54,13 @@ class CmdGC(CmdBase):
|
||||
msg += " and all experiments"
|
||||
|
||||
if self.args.not_in_remote:
|
||||
msg += " that are present in the DVC remote"
|
||||
msg += " that are not present in the DVC remote"
|
||||
|
||||
if self.args.repos:
|
||||
msg += " of the current and the following repos:"
|
||||
|
||||
for repo_path in self.args.repos:
|
||||
msg += "\n - %s" % os.path.abspath(repo_path)
|
||||
msg += f"\n - {os.path.abspath(repo_path)}"
|
||||
else:
|
||||
msg += " of the current repo."
|
||||
|
||||
@@ -188,7 +188,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to collect garbage in",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
gc_parser.add_argument(
|
||||
"--skip-failed",
|
||||
action="store_true",
|
||||
|
||||
+8
-2
@@ -12,7 +12,13 @@ class CmdGet(CmdBaseNoRepo):
|
||||
from dvc.api import get_url
|
||||
from dvc.ui import ui
|
||||
|
||||
url = get_url(self.args.path, repo=self.args.url, rev=self.args.rev)
|
||||
url = get_url(
|
||||
self.args.path,
|
||||
repo=self.args.url,
|
||||
rev=self.args.rev,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
)
|
||||
ui.write(url, force=True)
|
||||
|
||||
return 0
|
||||
@@ -115,7 +121,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
get_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
import_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
|
||||
@@ -73,7 +73,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to download to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
no_download_exec_group = import_parser.add_mutually_exclusive_group()
|
||||
no_download_exec_group.add_argument(
|
||||
"--no-exec",
|
||||
|
||||
+13
-4
@@ -42,7 +42,7 @@ class CmdInit(CmdBaseNoRepo):
|
||||
|
||||
try:
|
||||
with Repo.init(
|
||||
".",
|
||||
self.args.directory,
|
||||
no_scm=self.args.no_scm,
|
||||
force=self.args.force,
|
||||
subdir=self.args.subdir,
|
||||
@@ -57,10 +57,11 @@ class CmdInit(CmdBaseNoRepo):
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
"""Setup parser for `dvc init`."""
|
||||
INIT_HELP = "Initialize DVC in the current directory."
|
||||
INIT_HELP = "Initialize DVC repository."
|
||||
INIT_DESCRIPTION = (
|
||||
"Initialize DVC in the current directory. Expects directory\n"
|
||||
"to be a Git repository unless --no-scm option is specified."
|
||||
"Initialize DVC repository in the given directory (defaults to the current "
|
||||
"working directory).\n"
|
||||
"Expects directory to be a Git repository unless --no-scm option is specified."
|
||||
)
|
||||
|
||||
init_parser = subparsers.add_parser(
|
||||
@@ -70,6 +71,14 @@ def add_parser(subparsers, parent_parser):
|
||||
help=INIT_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"directory",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help=(
|
||||
"Directory to initialize DVC in. Defaults to the current working directory."
|
||||
),
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--no-scm",
|
||||
action="store_true",
|
||||
|
||||
+178
-33
@@ -1,3 +1,5 @@
|
||||
from typing import Callable
|
||||
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
@@ -9,54 +11,178 @@ from dvc.ui import ui
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def _format_entry(entry, fmt):
|
||||
from dvc.utils.humanize import naturalsize
|
||||
def _get_formatter(with_color: bool = False) -> Callable[[dict], str]:
|
||||
def fmt(entry: dict) -> str:
|
||||
return entry["path"]
|
||||
|
||||
size = entry.get("size")
|
||||
if size is None:
|
||||
size = ""
|
||||
else:
|
||||
size = naturalsize(size)
|
||||
return size, fmt(entry)
|
||||
|
||||
|
||||
def show_entries(entries, with_color=False, with_size=False):
|
||||
if with_color:
|
||||
ls_colors = LsColors()
|
||||
fmt = ls_colors.format
|
||||
else:
|
||||
return ls_colors.format
|
||||
|
||||
def fmt(entry):
|
||||
return entry["path"]
|
||||
return fmt
|
||||
|
||||
|
||||
def _format_entry(entry, name, with_size=True, with_hash=False):
|
||||
from dvc.utils.humanize import naturalsize
|
||||
|
||||
ret = []
|
||||
if with_size:
|
||||
ui.table([_format_entry(entry, fmt) for entry in entries])
|
||||
size = entry.get("size")
|
||||
if size is None or (size <= 0 and entry.get("isdir")):
|
||||
size = ""
|
||||
else:
|
||||
size = naturalsize(size)
|
||||
ret.append(size)
|
||||
if with_hash:
|
||||
md5 = entry.get("md5", "")
|
||||
ret.append(md5)
|
||||
ret.append(name)
|
||||
return ret
|
||||
|
||||
|
||||
def show_entries(entries, with_color=False, with_size=False, with_hash=False):
|
||||
fmt = _get_formatter(with_color)
|
||||
if with_size or with_hash:
|
||||
colalign = ("right",) if with_size else None
|
||||
ui.table(
|
||||
[
|
||||
_format_entry(
|
||||
entry,
|
||||
fmt(entry),
|
||||
with_size=with_size,
|
||||
with_hash=with_hash,
|
||||
)
|
||||
for entry in entries
|
||||
],
|
||||
colalign=colalign,
|
||||
)
|
||||
return
|
||||
|
||||
# NOTE: this is faster than ui.table for very large number of entries
|
||||
ui.write("\n".join(fmt(entry) for entry in entries))
|
||||
|
||||
|
||||
class TreePart:
|
||||
Edge = "├── "
|
||||
Line = "│ "
|
||||
Corner = "└── "
|
||||
Blank = " "
|
||||
|
||||
|
||||
def _build_tree_structure(
|
||||
entries, with_color=False, with_size=False, with_hash=False, _depth=0, _prefix=""
|
||||
):
|
||||
rows = []
|
||||
fmt = _get_formatter(with_color)
|
||||
|
||||
num_entries = len(entries)
|
||||
for i, (name, entry) in enumerate(entries.items()):
|
||||
entry["path"] = name
|
||||
is_last = i >= num_entries - 1
|
||||
tree_part = ""
|
||||
if _depth > 0:
|
||||
tree_part = TreePart.Corner if is_last else TreePart.Edge
|
||||
|
||||
row = _format_entry(
|
||||
entry,
|
||||
_prefix + tree_part + fmt(entry),
|
||||
with_size=with_size,
|
||||
with_hash=with_hash,
|
||||
)
|
||||
rows.append(row)
|
||||
|
||||
if contents := entry.get("contents"):
|
||||
new_prefix = _prefix
|
||||
if _depth > 0:
|
||||
new_prefix += TreePart.Blank if is_last else TreePart.Line
|
||||
new_rows = _build_tree_structure(
|
||||
contents,
|
||||
with_color=with_color,
|
||||
with_size=with_size,
|
||||
with_hash=with_hash,
|
||||
_depth=_depth + 1,
|
||||
_prefix=new_prefix,
|
||||
)
|
||||
rows.extend(new_rows)
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
def show_tree(entries, with_color=False, with_size=False, with_hash=False):
|
||||
import tabulate
|
||||
|
||||
rows = _build_tree_structure(
|
||||
entries,
|
||||
with_color=with_color,
|
||||
with_size=with_size,
|
||||
with_hash=with_hash,
|
||||
)
|
||||
|
||||
colalign = ("right",) if with_size else None
|
||||
|
||||
_orig = tabulate.PRESERVE_WHITESPACE
|
||||
tabulate.PRESERVE_WHITESPACE = True
|
||||
try:
|
||||
ui.table(rows, colalign=colalign)
|
||||
finally:
|
||||
tabulate.PRESERVE_WHITESPACE = _orig
|
||||
|
||||
|
||||
class CmdList(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
def _show_tree(self):
|
||||
from dvc.repo.ls import ls_tree
|
||||
|
||||
entries = ls_tree(
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
rev=self.args.rev,
|
||||
dvc_only=self.args.dvc_only,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
maxdepth=self.args.level,
|
||||
)
|
||||
show_tree(
|
||||
entries,
|
||||
with_color=True,
|
||||
with_size=self.args.size,
|
||||
with_hash=self.args.show_hash,
|
||||
)
|
||||
return 0
|
||||
|
||||
def _show_list(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
entries = Repo.ls(
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
entries = Repo.ls(
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
config=self.args.config,
|
||||
remote=self.args.remote,
|
||||
remote_config=self.args.remote_config,
|
||||
maxdepth=self.args.level,
|
||||
)
|
||||
if self.args.json:
|
||||
ui.write_json(entries)
|
||||
elif entries:
|
||||
show_entries(
|
||||
entries,
|
||||
with_color=True,
|
||||
with_size=self.args.size,
|
||||
with_hash=self.args.show_hash,
|
||||
)
|
||||
if self.args.json:
|
||||
ui.write_json(entries)
|
||||
elif entries:
|
||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
||||
return 0
|
||||
return 0
|
||||
|
||||
def run(self):
|
||||
if self.args.tree and self.args.json:
|
||||
raise DvcException("Cannot use --tree and --json options together.")
|
||||
|
||||
try:
|
||||
if self.args.tree:
|
||||
return self._show_tree()
|
||||
return self._show_list()
|
||||
except FileNotFoundError:
|
||||
logger.exception("")
|
||||
return 1
|
||||
@@ -85,6 +211,19 @@ def add_parser(subparsers, parent_parser):
|
||||
action="store_true",
|
||||
help="Recursively list files.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"-T",
|
||||
"--tree",
|
||||
action="store_true",
|
||||
help="Recurse into directories as a tree.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"-L",
|
||||
"--level",
|
||||
metavar="depth",
|
||||
type=int,
|
||||
help="Limit the depth of recursion.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--dvc-only", action="store_true", help="Show only DVC outputs."
|
||||
)
|
||||
@@ -111,7 +250,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
type=str,
|
||||
help="Remote name to set as a default in the target repository.",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
list_parser.add_argument(
|
||||
"--remote-config",
|
||||
type=str,
|
||||
@@ -123,6 +262,12 @@ def add_parser(subparsers, parent_parser):
|
||||
),
|
||||
)
|
||||
list_parser.add_argument("--size", action="store_true", help="Show sizes.")
|
||||
list_parser.add_argument(
|
||||
"--show-hash",
|
||||
help="Display hash value for each item.",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
|
||||
@@ -32,7 +32,9 @@ class LsColors:
|
||||
if entry.get("isexec", False):
|
||||
return self._format(text, code="ex")
|
||||
|
||||
_, ext = os.path.splitext(text)
|
||||
stem, ext = os.path.splitext(text)
|
||||
if not ext and stem.startswith("."):
|
||||
ext = stem
|
||||
return self._format(text, ext=ext)
|
||||
|
||||
def _format(self, text, code=None, ext=None):
|
||||
|
||||
+36
-4
@@ -3,26 +3,45 @@ from dvc.cli.command import CmdBaseNoRepo
|
||||
from dvc.cli.utils import DictAction, append_doc_link
|
||||
from dvc.log import logger
|
||||
|
||||
from .ls import show_entries
|
||||
from .ls import show_entries, show_tree
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
class CmdListUrl(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.config import Config
|
||||
def _show_tree(self, config):
|
||||
from dvc.fs import parse_external_url
|
||||
from dvc.repo.ls import _ls_tree
|
||||
|
||||
fs, fs_path = parse_external_url(
|
||||
self.args.url, fs_config=self.args.fs_config, config=config
|
||||
)
|
||||
entries = _ls_tree(fs, fs_path, maxdepth=self.args.level)
|
||||
show_tree(entries, with_color=True, with_size=self.args.size)
|
||||
return 0
|
||||
|
||||
def _show_list(self, config):
|
||||
from dvc.repo import Repo
|
||||
|
||||
entries = Repo.ls_url(
|
||||
self.args.url,
|
||||
recursive=self.args.recursive,
|
||||
maxdepth=self.args.level,
|
||||
fs_config=self.args.fs_config,
|
||||
config=Config.from_cwd(),
|
||||
config=config,
|
||||
)
|
||||
if entries:
|
||||
show_entries(entries, with_color=True, with_size=self.args.size)
|
||||
return 0
|
||||
|
||||
def run(self):
|
||||
from dvc.config import Config
|
||||
|
||||
config = Config.from_cwd()
|
||||
if self.args.tree:
|
||||
return self._show_tree(config=config)
|
||||
return self._show_list(config=config)
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
LS_HELP = "List directory contents from URL."
|
||||
@@ -40,6 +59,19 @@ def add_parser(subparsers, parent_parser):
|
||||
lsurl_parser.add_argument(
|
||||
"-R", "--recursive", action="store_true", help="Recursively list files."
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"-T",
|
||||
"--tree",
|
||||
action="store_true",
|
||||
help="Recurse into directories as a tree.",
|
||||
)
|
||||
lsurl_parser.add_argument(
|
||||
"-L",
|
||||
"--level",
|
||||
metavar="depth",
|
||||
type=int,
|
||||
help="Limit the depth of recursion.",
|
||||
)
|
||||
lsurl_parser.add_argument("--size", action="store_true", help="Show sizes.")
|
||||
lsurl_parser.add_argument(
|
||||
"--fs-config",
|
||||
|
||||
@@ -72,7 +72,7 @@ class CmdPlots(CmdBase):
|
||||
html_template_path = os.path.join(self.repo.dvc_dir, html_template_path)
|
||||
return html_template_path
|
||||
|
||||
def run(self) -> int: # noqa: C901, PLR0911, PLR0912
|
||||
def run(self) -> int: # noqa: C901, PLR0911
|
||||
from pathlib import Path
|
||||
|
||||
from dvc.render.match import match_defs_renderers
|
||||
|
||||
+22
-6
@@ -1,4 +1,4 @@
|
||||
from dvc.cli import formatter
|
||||
from dvc.cli import completion, formatter
|
||||
from dvc.cli.utils import append_doc_link
|
||||
from dvc.commands.config import CmdConfig
|
||||
from dvc.ui import ui
|
||||
@@ -110,8 +110,16 @@ class CmdRemoteDefault(CmdRemote):
|
||||
class CmdRemoteList(CmdRemote):
|
||||
def run(self):
|
||||
conf = self.config.read(self.args.level)
|
||||
default_remote = conf["core"].get("remote")
|
||||
|
||||
for name, remote_conf in conf["remote"].items():
|
||||
ui.write(name, remote_conf["url"], sep="\t")
|
||||
if name == default_remote:
|
||||
text = f"{name}\t{remote_conf['url']}\t(default)"
|
||||
color = "green"
|
||||
else:
|
||||
text = f"{name}\t{remote_conf['url']}"
|
||||
color = ""
|
||||
ui.write(ui.rich_text(text, style=color), styled=True)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -202,7 +210,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOTE_DEFAULT_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_default_parser.add_argument("name", nargs="?", help="Name of the remote")
|
||||
remote_default_parser.add_argument(
|
||||
"name", nargs="?", help="Name of the remote"
|
||||
).complete = completion.REMOTE
|
||||
remote_default_parser.add_argument(
|
||||
"-u",
|
||||
"--unset",
|
||||
@@ -220,8 +230,12 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOTE_MODIFY_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_modify_parser.add_argument("name", help="Name of the remote")
|
||||
remote_modify_parser.add_argument("option", help="Name of the option to modify.")
|
||||
remote_modify_parser.add_argument(
|
||||
"name", help="Name of the remote"
|
||||
).complete = completion.REMOTE
|
||||
remote_modify_parser.add_argument(
|
||||
"option", help="Name of the option to modify."
|
||||
).complete = completion.REMOTE_CONFIG_VARS
|
||||
remote_modify_parser.add_argument(
|
||||
"value", nargs="?", help="(optional) Value of the option."
|
||||
)
|
||||
@@ -252,7 +266,9 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOTE_REMOVE_HELP,
|
||||
formatter_class=formatter.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_remove_parser.add_argument("name", help="Name of the remote to remove.")
|
||||
remote_remove_parser.add_argument(
|
||||
"name", help="Name of the remote to remove."
|
||||
).complete = completion.REMOTE
|
||||
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
|
||||
REMOTE_RENAME_HELP = "Rename a DVC remote"
|
||||
remote_rename_parser = remote_subparsers.add_parser(
|
||||
|
||||
@@ -117,10 +117,7 @@ and then the stage name name.
|
||||
"--pull",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Try automatically pulling missing cache for outputs restored "
|
||||
"from the run-cache."
|
||||
),
|
||||
help="Try automatically pulling missing data.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--allow-missing",
|
||||
|
||||
@@ -68,7 +68,7 @@ def prepare_stages_data(
|
||||
class CmdStageList(CmdBase):
|
||||
def _get_stages(self) -> Iterable["Stage"]:
|
||||
if self.args.all:
|
||||
stages: list["Stage"] = self.repo.index.stages
|
||||
stages: list[Stage] = self.repo.index.stages
|
||||
logger.trace("%d no. of stages found", len(stages))
|
||||
return stages
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
return 0
|
||||
|
||||
if self.args.quiet:
|
||||
return bool(st)
|
||||
return int(bool(st))
|
||||
|
||||
if st:
|
||||
self._show(st, indent)
|
||||
|
||||
+21
-6
@@ -13,17 +13,21 @@ logger = logger.getChild(__name__)
|
||||
|
||||
class CmdStudioLogin(CmdConfig):
|
||||
def run(self):
|
||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||
|
||||
from dvc.env import DVC_STUDIO_URL
|
||||
from dvc.ui import ui
|
||||
from dvc.utils.studio import STUDIO_URL
|
||||
|
||||
name = self.args.name
|
||||
hostname = self.args.hostname or os.environ.get(DVC_STUDIO_URL) or STUDIO_URL
|
||||
scopes = self.args.scopes
|
||||
from dvc_studio_client.auth import StudioAuthError, get_access_token
|
||||
|
||||
studio = self.config.get("studio", {})
|
||||
name = self.args.name
|
||||
hostname = (
|
||||
self.args.hostname
|
||||
or os.environ.get(DVC_STUDIO_URL)
|
||||
or studio.get("url")
|
||||
or STUDIO_URL
|
||||
)
|
||||
scopes = self.args.scopes
|
||||
|
||||
if studio.get("url", hostname) == hostname and "token" in studio:
|
||||
raise DvcException(
|
||||
"Token already exists. "
|
||||
@@ -46,6 +50,15 @@ class CmdStudioLogin(CmdConfig):
|
||||
|
||||
self.save_config(hostname, access_token)
|
||||
|
||||
if not self.config["exp"].get("auto_push", True):
|
||||
from dvc.ui import ui
|
||||
|
||||
ui.warn(
|
||||
"exp.auto_push is disabled. \n"
|
||||
"Enable with 'dvc config exp.auto_push true' "
|
||||
"to automatically push experiments to Studio."
|
||||
)
|
||||
|
||||
config_path = self.config.files["global"]
|
||||
ui.write(f"Authentication complete. Saved token to {config_path}.")
|
||||
return 0
|
||||
@@ -54,6 +67,8 @@ class CmdStudioLogin(CmdConfig):
|
||||
with self.config.edit("global") as conf:
|
||||
conf["studio"]["token"] = token
|
||||
conf["studio"]["url"] = hostname
|
||||
if "auto_push" not in conf["exp"]:
|
||||
conf["exp"]["auto_push"] = True
|
||||
|
||||
|
||||
class CmdStudioLogout(CmdConfig):
|
||||
|
||||
@@ -74,7 +74,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--remote",
|
||||
help="Remote storage to perform updates to",
|
||||
metavar="<name>",
|
||||
)
|
||||
).complete = completion.REMOTE
|
||||
update_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
|
||||
+3
-5
@@ -88,12 +88,10 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
return list(it)
|
||||
|
||||
@overload
|
||||
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None:
|
||||
...
|
||||
def __setitem__(self, item: int, value: Sequence["CellT"]) -> None: ...
|
||||
|
||||
@overload
|
||||
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None:
|
||||
...
|
||||
def __setitem__(self, item: slice, value: Iterable[Sequence["CellT"]]) -> None: ...
|
||||
|
||||
def __setitem__(self, item, value) -> None:
|
||||
it = value
|
||||
@@ -162,7 +160,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
|
||||
if key not in keys:
|
||||
self.add_column(key)
|
||||
|
||||
row: list["CellT"] = [
|
||||
row: list[CellT] = [
|
||||
with_value(d.get(key), self._fill_value) for key in self.keys()
|
||||
]
|
||||
self.append(row)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"""DVC config objects."""
|
||||
|
||||
import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
@@ -136,6 +139,7 @@ class Config(dict):
|
||||
return global_config_dir()
|
||||
if level == "system":
|
||||
return system_config_dir()
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def files(self) -> dict[str, str]:
|
||||
@@ -267,12 +271,17 @@ class Config(dict):
|
||||
if re.match(r"\w+://", path):
|
||||
return path
|
||||
|
||||
if os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path:
|
||||
return path
|
||||
|
||||
if os.path.isabs(path):
|
||||
return path
|
||||
|
||||
# on windows convert slashes to backslashes
|
||||
# to have path compatible with abs_conf_dir
|
||||
if os.path.sep == "\\" and "/" in path:
|
||||
if path.startswith("/"):
|
||||
path = path.replace("/", "\\\\", 1)
|
||||
path = path.replace("/", "\\")
|
||||
|
||||
expanded = os.path.expanduser(path)
|
||||
@@ -304,6 +313,9 @@ class Config(dict):
|
||||
if os.path.expanduser(path) != path:
|
||||
return localfs.as_posix(path)
|
||||
|
||||
if os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path:
|
||||
return path
|
||||
|
||||
if isinstance(path, RelPath) or not os.path.isabs(path):
|
||||
path = relpath(path, conf_dir)
|
||||
return localfs.as_posix(path)
|
||||
@@ -329,6 +341,7 @@ class Config(dict):
|
||||
"gdrive_user_credentials_file": func,
|
||||
"gdrive_service_account_json_file_path": func,
|
||||
"credentialpath": func,
|
||||
"configpath": func,
|
||||
"keyfile": func,
|
||||
"cert_path": func,
|
||||
"key_path": func,
|
||||
|
||||
+205
-145
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import once, walk_values
|
||||
@@ -10,6 +11,7 @@ from voluptuous import (
|
||||
Exclusive,
|
||||
Invalid,
|
||||
Lower,
|
||||
Marker,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
@@ -17,6 +19,9 @@ from voluptuous import (
|
||||
|
||||
from dvc.log import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
Bool = All(
|
||||
@@ -55,23 +60,27 @@ def Choices(*choices): # noqa: N802
|
||||
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
|
||||
|
||||
|
||||
def _get_schema_from_url(url: str) -> str:
|
||||
parsed = urlparse(url)
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
|
||||
return ""
|
||||
if not parsed.netloc:
|
||||
return ""
|
||||
return parsed.scheme
|
||||
|
||||
|
||||
def ByUrl(mapping): # noqa: N802
|
||||
schemas = walk_values(Schema, mapping)
|
||||
schemas: dict[str, Schema] = walk_values(Schema, mapping)
|
||||
|
||||
def validate(data):
|
||||
if "url" not in data:
|
||||
raise Invalid("expected 'url'")
|
||||
|
||||
parsed = urlparse(data["url"])
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if not parsed.netloc:
|
||||
return schemas[""](data)
|
||||
if parsed.scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
|
||||
|
||||
return schemas[parsed.scheme](data)
|
||||
scheme = _get_schema_from_url(data["url"])
|
||||
if scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {scheme}://")
|
||||
return schemas[scheme](data)
|
||||
|
||||
return validate
|
||||
|
||||
@@ -112,17 +121,19 @@ class FeatureSchema(Schema):
|
||||
return ret
|
||||
|
||||
|
||||
DEPRECATED = "==DEPRECATED=="
|
||||
|
||||
REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
"jobs": All(Coerce(int), Range(1)),
|
||||
Optional("worktree"): Bool,
|
||||
Optional("no_traverse"): Bool, # obsoleted
|
||||
Optional("no_traverse", description=DEPRECATED): Bool, # obsoleted
|
||||
Optional("version_aware"): Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
Optional("protected", default=False): Bool, # obsoleted
|
||||
Optional("protected", default=False, description=DEPRECATED): Bool, # obsoleted
|
||||
"shared": All(Lower, Choices("group")),
|
||||
Optional("slow_link_warning", default=True): Bool,
|
||||
Optional("verify", default=False): Bool,
|
||||
@@ -152,6 +163,127 @@ WEBDAV_COMMON = {
|
||||
Optional("verify", default=False): Bool,
|
||||
}
|
||||
|
||||
REMOTE_SCHEMAS = {
|
||||
"": LOCAL_COMMON | REMOTE_COMMON,
|
||||
"s3": {
|
||||
"region": str,
|
||||
"profile": str,
|
||||
"credentialpath": str,
|
||||
"configpath": str,
|
||||
"endpointurl": str,
|
||||
"access_key_id": str,
|
||||
"secret_access_key": str,
|
||||
"session_token": str,
|
||||
Optional(
|
||||
"listobjects", default=False, description=DEPRECATED
|
||||
): Bool, # obsoleted
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"sse_customer_algorithm": str,
|
||||
"sse_customer_key": str,
|
||||
"acl": str,
|
||||
"grant_read": str,
|
||||
"grant_read_acp": str,
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
"cache_regions": bool,
|
||||
"read_timeout": Coerce(int),
|
||||
"connect_timeout": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
Optional("allow_anonymous_login", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"ssh": {
|
||||
"type": supported_cache_type,
|
||||
"port": Coerce(int),
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"hdfs": {
|
||||
"user": str,
|
||||
"kerb_ticket": str,
|
||||
"replication": int,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"webhdfs": {
|
||||
"kerberos": Bool,
|
||||
"kerberos_principal": str,
|
||||
"proxy_to": str,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"token": str,
|
||||
"use_https": Bool,
|
||||
"user": str,
|
||||
"password": str,
|
||||
"data_proxy_target": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"azure": {
|
||||
"connection_string": str,
|
||||
"sas_token": str,
|
||||
"account_name": str,
|
||||
"account_key": str,
|
||||
"tenant_id": str,
|
||||
"client_id": str,
|
||||
"client_secret": str,
|
||||
"allow_anonymous_login": Bool,
|
||||
"exclude_environment_credential": Bool,
|
||||
"exclude_visual_studio_code_credential": Bool,
|
||||
"exclude_shared_token_cache_credential": Bool,
|
||||
"exclude_managed_identity_credential": Bool,
|
||||
Optional("verify", default=False): Bool,
|
||||
"timeout": Coerce(int),
|
||||
"read_timeout": Coerce(int),
|
||||
"connection_timeout": Coerce(int),
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"oss": {
|
||||
"oss_key_id": str,
|
||||
"oss_key_secret": str,
|
||||
"oss_endpoint": str,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gdrive": {
|
||||
"profile": str,
|
||||
"gdrive_use_service_account": Bool,
|
||||
"gdrive_client_id": str,
|
||||
"gdrive_client_secret": str,
|
||||
"gdrive_user_credentials_file": str,
|
||||
"gdrive_service_account_user_email": str,
|
||||
"gdrive_service_account_json_file_path": str,
|
||||
Optional("gdrive_trash_only", default=False): Bool,
|
||||
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"http": HTTP_COMMON | REMOTE_COMMON,
|
||||
"https": HTTP_COMMON | REMOTE_COMMON,
|
||||
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"remote": {str: object}, # Any of the above options are valid
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
"core": {
|
||||
"remote": Lower,
|
||||
@@ -161,145 +293,37 @@ SCHEMA = {
|
||||
Optional("hardlink_lock", default=False): Bool,
|
||||
Optional("no_scm", default=False): Bool,
|
||||
Optional("autostage", default=False): Bool,
|
||||
Optional("experiments"): Bool, # obsoleted
|
||||
Optional("experiments", description=DEPRECATED): Bool, # obsoleted
|
||||
Optional("check_update", default=True): Bool,
|
||||
"site_cache_dir": str,
|
||||
"machine": Lower,
|
||||
},
|
||||
"cache": {
|
||||
"local": str, # obsoleted
|
||||
"s3": str, # obsoleted
|
||||
"gs": str, # obsoleted
|
||||
"hdfs": str, # obsoleted
|
||||
"webhdfs": str, # obsoleted
|
||||
"ssh": str, # obsoleted
|
||||
"azure": str, # obsoleted
|
||||
Marker("local", description=DEPRECATED): str, # obsoleted
|
||||
Marker("s3", description=DEPRECATED): str, # obsoleted
|
||||
Marker("gs", description=DEPRECATED): str, # obsoleted
|
||||
Marker("hdfs", description=DEPRECATED): str, # obsoleted
|
||||
Marker("webhdfs", description=DEPRECATED): str, # obsoleted
|
||||
Marker("ssh", description=DEPRECATED): str, # obsoleted
|
||||
Marker("azure", description=DEPRECATED): str, # obsoleted
|
||||
# This is for default local cache
|
||||
"dir": str,
|
||||
**LOCAL_COMMON,
|
||||
},
|
||||
"remote": {
|
||||
str: ByUrl(
|
||||
{
|
||||
"": LOCAL_COMMON | REMOTE_COMMON,
|
||||
"s3": {
|
||||
"region": str,
|
||||
"profile": str,
|
||||
"credentialpath": str,
|
||||
"configpath": str,
|
||||
"endpointurl": str,
|
||||
"access_key_id": str,
|
||||
"secret_access_key": str,
|
||||
"session_token": str,
|
||||
Optional("listobjects", default=False): Bool, # obsoleted
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"sse_customer_algorithm": str,
|
||||
"sse_customer_key": str,
|
||||
"acl": str,
|
||||
"grant_read": str,
|
||||
"grant_read_acp": str,
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
"cache_regions": bool,
|
||||
"read_timeout": Coerce(int),
|
||||
"connect_timeout": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"ssh": {
|
||||
"type": supported_cache_type,
|
||||
"port": Coerce(int),
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"passphrase": str,
|
||||
"ask_passphrase": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
"allow_agent": Bool,
|
||||
"max_sessions": Coerce(int),
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
|
||||
"webhdfs": {
|
||||
"kerberos": Bool,
|
||||
"kerberos_principal": str,
|
||||
"proxy_to": str,
|
||||
"ssl_verify": Any(Bool, str),
|
||||
"token": str,
|
||||
"use_https": Bool,
|
||||
"user": str,
|
||||
"password": str,
|
||||
"data_proxy_target": str,
|
||||
Optional("verify", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"azure": {
|
||||
"connection_string": str,
|
||||
"sas_token": str,
|
||||
"account_name": str,
|
||||
"account_key": str,
|
||||
"tenant_id": str,
|
||||
"client_id": str,
|
||||
"client_secret": str,
|
||||
"allow_anonymous_login": Bool,
|
||||
"exclude_environment_credential": Bool,
|
||||
"exclude_visual_studio_code_credential": Bool,
|
||||
"exclude_shared_token_cache_credential": Bool,
|
||||
"exclude_managed_identity_credential": Bool,
|
||||
Optional("verify", default=False): Bool,
|
||||
"timeout": Coerce(int),
|
||||
"read_timeout": Coerce(int),
|
||||
"connection_timeout": Coerce(int),
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"oss": {
|
||||
"oss_key_id": str,
|
||||
"oss_key_secret": str,
|
||||
"oss_endpoint": str,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gdrive": {
|
||||
"profile": str,
|
||||
"gdrive_use_service_account": Bool,
|
||||
"gdrive_client_id": str,
|
||||
"gdrive_client_secret": str,
|
||||
"gdrive_user_credentials_file": str,
|
||||
"gdrive_service_account_user_email": str,
|
||||
"gdrive_service_account_json_file_path": str,
|
||||
Optional("gdrive_trash_only", default=False): Bool,
|
||||
Optional("gdrive_acknowledge_abuse", default=False): Bool,
|
||||
Optional("verify", default=True): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"http": HTTP_COMMON | REMOTE_COMMON,
|
||||
"https": HTTP_COMMON | REMOTE_COMMON,
|
||||
"webdav": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"webdavs": WEBDAV_COMMON | REMOTE_COMMON,
|
||||
"remote": {str: object}, # Any of the above options are valid
|
||||
}
|
||||
)
|
||||
str: ByUrl(REMOTE_SCHEMAS),
|
||||
},
|
||||
"state": {
|
||||
"dir": str, # obsoleted
|
||||
"row_limit": All(Coerce(int), Range(1)), # obsoleted
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
|
||||
Marker("dir", description=DEPRECATED): str, # obsoleted
|
||||
Marker("row_limit", description=DEPRECATED): All(
|
||||
Coerce(int), Range(1)
|
||||
), # obsoleted
|
||||
Marker("row_cleanup_quota", description=DEPRECATED): All(
|
||||
Coerce(int), Range(0, 100)
|
||||
), # obsoleted
|
||||
},
|
||||
"index": {
|
||||
"dir": str, # obsoleted
|
||||
Marker("dir", description=DEPRECATED): str, # obsoleted
|
||||
},
|
||||
"machine": {
|
||||
str: {
|
||||
@@ -329,13 +353,15 @@ SCHEMA = {
|
||||
"out_dir": str,
|
||||
},
|
||||
"exp": {
|
||||
"code": str,
|
||||
"data": str,
|
||||
"models": str,
|
||||
"metrics": str,
|
||||
"params": str,
|
||||
"plots": str,
|
||||
"live": str,
|
||||
Marker("code", description=DEPRECATED): str,
|
||||
Marker("data", description=DEPRECATED): str,
|
||||
Marker("models", description=DEPRECATED): str,
|
||||
Marker("metrics", description=DEPRECATED): str,
|
||||
Marker("params", description=DEPRECATED): str,
|
||||
Marker("plots", description=DEPRECATED): str,
|
||||
Marker("live", description=DEPRECATED): str,
|
||||
"auto_push": Bool,
|
||||
"git_remote": str,
|
||||
},
|
||||
"parsing": {
|
||||
"bool": All(Lower, Choices("store_true", "boolean_optional")),
|
||||
@@ -362,3 +388,37 @@ SCHEMA = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def config_vars_for_completion(d: dict = SCHEMA, path: str = "") -> "Iterator[str]":
|
||||
for k, v in d.items():
|
||||
if k in ("machine", "feature"):
|
||||
continue
|
||||
if isinstance(k, Marker):
|
||||
if k.description == DEPRECATED:
|
||||
continue
|
||||
k = k.schema
|
||||
if not isinstance(k, str):
|
||||
continue
|
||||
keypath = f"{path}.{k}" if path else k
|
||||
if isinstance(v, dict):
|
||||
yield from config_vars_for_completion(v, keypath)
|
||||
else:
|
||||
yield keypath
|
||||
|
||||
|
||||
def contextual_config_vars_for_completion(config) -> "Iterator[str]":
|
||||
yield from config_vars_for_completion()
|
||||
for name, data in config["remote"].items():
|
||||
if "url" not in data:
|
||||
continue
|
||||
scheme = _get_schema_from_url(data["url"])
|
||||
if scheme not in REMOTE_SCHEMAS:
|
||||
continue
|
||||
schema = REMOTE_SCHEMAS[scheme]
|
||||
if schema:
|
||||
yield from config_vars_for_completion(schema, f"remote.{name}") # type: ignore[arg-type]
|
||||
|
||||
db_schema = SCHEMA["db"][str] # type: ignore[index]
|
||||
for name in config["db"]:
|
||||
yield from config_vars_for_completion(db_schema, f"db.{name}") # type: ignore[arg-type]
|
||||
|
||||
+8
-8
@@ -32,10 +32,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
||||
assert os.name == "nt"
|
||||
|
||||
from subprocess import ( # type: ignore[attr-defined]
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
STARTUPINFO,
|
||||
CREATE_NEW_PROCESS_GROUP, # ty: ignore[unresolved-import]
|
||||
CREATE_NO_WINDOW, # ty: ignore[unresolved-import]
|
||||
STARTF_USESHOWWINDOW, # ty: ignore[unresolved-import]
|
||||
STARTUPINFO, # ty: ignore[unresolved-import]
|
||||
)
|
||||
|
||||
# https://stackoverflow.com/a/7006424
|
||||
@@ -44,10 +44,10 @@ def _win_detached_subprocess(args: Sequence[str], **kwargs) -> int:
|
||||
|
||||
startupinfo = STARTUPINFO()
|
||||
startupinfo.dwFlags |= STARTF_USESHOWWINDOW
|
||||
popen = subprocess.Popen(
|
||||
popen = subprocess.Popen( # noqa: S603
|
||||
args,
|
||||
close_fds=True,
|
||||
shell=False, # noqa: S603
|
||||
shell=False,
|
||||
startupinfo=startupinfo,
|
||||
creationflags=creationflags,
|
||||
**kwargs,
|
||||
@@ -149,7 +149,7 @@ def _spawn(
|
||||
env: Optional[Mapping[str, str]] = None,
|
||||
output_file: Optional[str] = None,
|
||||
) -> int:
|
||||
file: "AbstractContextManager[Any]" = nullcontext()
|
||||
file: AbstractContextManager[Any] = nullcontext()
|
||||
kwargs = {}
|
||||
if output_file:
|
||||
file = open(output_file, "ab") # noqa: SIM115
|
||||
@@ -164,7 +164,7 @@ def _spawn(
|
||||
return _detached_subprocess(executable + args, env=env, **kwargs)
|
||||
|
||||
|
||||
def daemonize(args: list[str], executable: Union[None, str, list[str]] = None) -> None:
|
||||
def daemonize(args: list[str], executable: Union[str, list[str], None] = None) -> None:
|
||||
if os.name not in ("posix", "nt"):
|
||||
return
|
||||
|
||||
|
||||
+13
-13
@@ -104,21 +104,21 @@ class AsciiCanvas:
|
||||
if dx == 0:
|
||||
y = y0
|
||||
else:
|
||||
y = y0 + int(round((x - x0) * dy / float(dx)))
|
||||
y = y0 + round((x - x0) * dy / float(dx))
|
||||
self.point(x, y, char)
|
||||
elif y0 < y1:
|
||||
for y in range(y0, y1 + 1):
|
||||
if dy == 0:
|
||||
x = x0
|
||||
else:
|
||||
x = x0 + int(round((y - y0) * dx / float(dy)))
|
||||
x = x0 + round((y - y0) * dx / float(dy))
|
||||
self.point(x, y, char)
|
||||
else:
|
||||
for y in range(y1, y0 + 1):
|
||||
if dy == 0:
|
||||
x = x0
|
||||
else:
|
||||
x = x1 + int(round((y - y1) * dx / float(dy)))
|
||||
x = x1 + round((y - y1) * dx / float(dy))
|
||||
self.point(x, y, char)
|
||||
|
||||
def text(self, x, y, text):
|
||||
@@ -195,7 +195,7 @@ def _build_sugiyama_layout(vertices, edges):
|
||||
|
||||
sug.yspace = VertexViewer.HEIGHT
|
||||
sug.xspace = minw
|
||||
sug.route_edge = route_with_lines
|
||||
sug.route_edge = route_with_lines # ty: ignore[unresolved-attribute]
|
||||
|
||||
sug.draw()
|
||||
|
||||
@@ -258,8 +258,8 @@ def draw(vertices, edges):
|
||||
maxx = max(Xs)
|
||||
maxy = max(Ys)
|
||||
|
||||
canvas_cols = int(math.ceil(math.ceil(maxx) - math.floor(minx))) + 1
|
||||
canvas_lines = int(round(maxy - miny))
|
||||
canvas_cols = math.ceil(math.ceil(maxx) - math.floor(minx)) + 1
|
||||
canvas_lines = round(maxy - miny)
|
||||
|
||||
canvas = AsciiCanvas(canvas_cols, canvas_lines)
|
||||
|
||||
@@ -270,10 +270,10 @@ def draw(vertices, edges):
|
||||
start = edge.view._pts[index - 1]
|
||||
end = edge.view._pts[index]
|
||||
|
||||
start_x = int(round(start[0] - minx))
|
||||
start_y = int(round(start[1] - miny))
|
||||
end_x = int(round(end[0] - minx))
|
||||
end_y = int(round(end[1] - miny))
|
||||
start_x = round(start[0] - minx)
|
||||
start_y = round(start[1] - miny)
|
||||
end_x = round(end[0] - minx)
|
||||
end_y = round(end[1] - miny)
|
||||
|
||||
assert start_x >= 0
|
||||
assert start_y >= 0
|
||||
@@ -288,12 +288,12 @@ def draw(vertices, edges):
|
||||
y = vertex.view.xy[1]
|
||||
|
||||
canvas.box(
|
||||
int(round(x - minx)),
|
||||
int(round(y - miny)),
|
||||
round(x - minx),
|
||||
round(y - miny),
|
||||
vertex.view.w,
|
||||
vertex.view.h,
|
||||
)
|
||||
|
||||
canvas.text(int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data)
|
||||
canvas.text(round(x - minx) + 1, round(y - miny) + 1, vertex.data)
|
||||
|
||||
return canvas.draw()
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class Client:
|
||||
except Exception as exc:
|
||||
if callable(onerror):
|
||||
onerror()
|
||||
logger.exception(
|
||||
logger.exception( # noqa: LOG007
|
||||
"Could not connect to the database. "
|
||||
"Check your database credentials and try again.",
|
||||
exc_info=False,
|
||||
|
||||
@@ -45,7 +45,7 @@ class Dependency(Output):
|
||||
self.fs_path = self.fs.version_path(self.fs_path, self.meta.version_id)
|
||||
|
||||
def download(self, to, jobs=None):
|
||||
fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
return fs_download(self.fs, self.fs_path, to.fs_path, jobs=jobs)
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
|
||||
+13
-6
@@ -22,9 +22,12 @@ def download_progress(to: "Output") -> Iterator[Callable[[int], Any]]:
|
||||
from dvc.ui import ui
|
||||
from dvc.ui._rich_progress import DbDownloadProgress
|
||||
|
||||
with log_durations(logger.debug, f"Saving to {to}"), DbDownloadProgress(
|
||||
console=ui.error_console,
|
||||
) as progress:
|
||||
with (
|
||||
log_durations(logger.debug, f"Saving to {to}"),
|
||||
DbDownloadProgress(
|
||||
console=ui.error_console,
|
||||
) as progress,
|
||||
):
|
||||
task = progress.add_task("Saving", total=None, output=to)
|
||||
yield lambda n: progress.advance(task, advance=n)
|
||||
progress.update(task, description="Saved", total=0)
|
||||
@@ -34,13 +37,17 @@ class AbstractDependency(Dependency):
|
||||
"""Dependency without workspace/fs/fs_path"""
|
||||
|
||||
def __init__(self, stage: "Stage", info: dict[str, Any], *args, **kwargs):
|
||||
self.repo: "Repo" = stage.repo
|
||||
self.repo: Repo = stage.repo
|
||||
self.stage = stage
|
||||
self.fs = None
|
||||
self.fs_path = None
|
||||
self.def_path = None # type: ignore[assignment]
|
||||
self.info = info or {}
|
||||
|
||||
@property
|
||||
def is_in_repo(self):
|
||||
return False
|
||||
|
||||
|
||||
class DbDependency(AbstractDependency):
|
||||
PARAM_CONNECTION = "connection"
|
||||
@@ -67,8 +74,8 @@ class DbDependency(AbstractDependency):
|
||||
return self.db_info.get(self.PARAM_QUERY) or self.db_info.get(self.PARAM_TABLE)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}:{}".format(
|
||||
self.__class__.__name__, "".join(f"{k}=={v}" for k, v in self.info.items())
|
||||
return "{}: {}".format(
|
||||
self.__class__.__name__, "".join(f"{k}={v}" for k, v in self.info.items())
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
+29
-2
@@ -9,7 +9,9 @@ from .base import Dependency
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import DVCFileSystem
|
||||
from dvc.output import Output
|
||||
from dvc.stage import Stage
|
||||
from dvc_data.hashfile.hash_info import HashInfo
|
||||
|
||||
|
||||
class RepoDependency(Dependency):
|
||||
@@ -35,7 +37,7 @@ class RepoDependency(Dependency):
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
self.fs = self._make_fs()
|
||||
self.fs_path = as_posix(self.def_path)
|
||||
self.fs_path = as_posix(self.fs.normpath(self.def_path))
|
||||
|
||||
def _parse_path(self, fs, fs_path): # noqa: ARG002
|
||||
return None
|
||||
@@ -91,6 +93,27 @@ class RepoDependency(Dependency):
|
||||
self.PARAM_REPO: self._dump_def_repo(self.def_repo),
|
||||
}
|
||||
|
||||
def download(self, to: "Output", jobs: Optional[int] = None):
|
||||
from dvc.fs import LocalFileSystem
|
||||
|
||||
files = super().download(to=to, jobs=jobs)
|
||||
if not isinstance(to.fs, LocalFileSystem):
|
||||
return
|
||||
|
||||
hashes: list[tuple[str, HashInfo, dict[str, Any]]] = []
|
||||
for src_path, dest_path, maybe_info in files:
|
||||
try:
|
||||
info = maybe_info or self.fs.info(src_path)
|
||||
hash_info = info["dvc_info"]["entry"].hash_info
|
||||
dest_info = to.fs.info(dest_path)
|
||||
except (KeyError, AttributeError):
|
||||
# If no hash info found, just keep going and output will be hashed later
|
||||
continue
|
||||
if hash_info:
|
||||
hashes.append((dest_path, hash_info, dest_info))
|
||||
cache = to.cache if to.use_cache else to.local_cache
|
||||
cache.state.save_many(hashes, to.fs)
|
||||
|
||||
def update(self, rev: Optional[str] = None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
@@ -123,11 +146,15 @@ class RepoDependency(Dependency):
|
||||
else:
|
||||
config = Config.load_file(conf)
|
||||
|
||||
# Setup config to the new DVCFileSystem to use the remote repo, but rely on the
|
||||
# local cache instead of the remote's cache. This avoids re-streaming of data,
|
||||
# but messes up the call to `_get_remote_config()` downstream, which will need
|
||||
# to ignore cache parameters.
|
||||
config["cache"] = self.repo.config["cache"]
|
||||
config["cache"]["dir"] = self.repo.cache.local_cache_dir
|
||||
|
||||
return DVCFileSystem(
|
||||
url=self.def_repo[self.PARAM_URL],
|
||||
repo=self.def_repo[self.PARAM_URL],
|
||||
rev=rev or self._get_rev(locked=locked),
|
||||
subrepos=True,
|
||||
config=config,
|
||||
|
||||
+22
-4
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import platformdirs
|
||||
|
||||
@@ -20,7 +21,24 @@ def global_config_dir():
|
||||
)
|
||||
|
||||
|
||||
def site_cache_dir():
|
||||
return os.getenv(env.DVC_SITE_CACHE_DIR) or platformdirs.site_cache_dir(
|
||||
APPNAME, APPAUTHOR, opinion=True
|
||||
)
|
||||
def site_cache_dir(config_site_cache_dir: Optional[str] = None):
|
||||
from platformdirs import PlatformDirs
|
||||
from platformdirs.unix import Unix
|
||||
|
||||
if dvc_site_cache_dir := os.getenv(env.DVC_SITE_CACHE_DIR):
|
||||
return dvc_site_cache_dir
|
||||
|
||||
if config_site_cache_dir is not None:
|
||||
return config_site_cache_dir
|
||||
|
||||
if issubclass(Unix, PlatformDirs):
|
||||
# Return the cache directory shared by users, e.g. `/var/tmp/$appname`
|
||||
# NOTE: platformdirs>=5 changed `site_cache_dir` to return /var/cache/$appname.
|
||||
# as the following path is considered insecure.
|
||||
# For details, see: https://github.com/tox-dev/platformdirs/pull/239
|
||||
|
||||
# FIXME: keeping the old behavior temporarily to avoid dependency conflict.
|
||||
# In the future, consider migrating to a more secure directory.
|
||||
return f"/var/tmp/{APPNAME}" # noqa: S108
|
||||
|
||||
return platformdirs.site_cache_dir(APPNAME, APPAUTHOR, opinion=True)
|
||||
|
||||
+87
-47
@@ -68,10 +68,9 @@ def is_git_ignored(repo, path):
|
||||
def check_dvcfile_path(repo, path):
|
||||
if not is_valid_filename(path):
|
||||
raise StageFileBadNameError(
|
||||
"bad DVC file name '{}'. DVC files should be named "
|
||||
"'{}' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
|
||||
relpath(path), PROJECT_FILE, os.path.basename(path)
|
||||
)
|
||||
f"bad DVC file name '{relpath(path)}'. DVC files should be named "
|
||||
f"'{PROJECT_FILE}' or have a '.dvc' suffix "
|
||||
f"(e.g. '{os.path.basename(path)}.dvc')."
|
||||
)
|
||||
|
||||
if is_git_ignored(repo, path):
|
||||
@@ -163,6 +162,9 @@ class FileMixin:
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def dump_stages(self, stages, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def merge(self, ancestor, other, allowed=None):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -199,6 +201,13 @@ class SingleStageFile(FileMixin):
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage, **kwargs))
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump_stages(self, stages, **kwargs) -> None:
|
||||
if not stages:
|
||||
return None
|
||||
|
||||
assert len(stages) == 1, "SingleStageFile can only dump one stage."
|
||||
return self.dump(stages[0], **kwargs)
|
||||
|
||||
def remove_stage(self, stage): # noqa: ARG002
|
||||
self.remove()
|
||||
|
||||
@@ -229,58 +238,79 @@ class ProjectFile(FileMixin):
|
||||
|
||||
def dump(self, stage, update_pipeline=True, update_lock=True, **kwargs):
|
||||
"""Dumps given stage appropriately in the dvcfile."""
|
||||
return self.dump_stages(
|
||||
[stage], update_pipeline=update_pipeline, update_lock=update_lock, **kwargs
|
||||
)
|
||||
|
||||
def dump_stages(self, stages, update_pipeline=True, update_lock=True, **kwargs):
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
assert isinstance(stage, PipelineStage)
|
||||
if not stages:
|
||||
return
|
||||
|
||||
for stage in stages:
|
||||
assert isinstance(stage, PipelineStage)
|
||||
|
||||
if self.verify:
|
||||
check_dvcfile_path(self.repo, self.path)
|
||||
|
||||
if update_pipeline and not stage.is_data_source:
|
||||
self._dump_pipeline_file(stage)
|
||||
if update_pipeline:
|
||||
self._dump_pipeline_file(stages)
|
||||
|
||||
if update_lock:
|
||||
self._dump_lockfile(stage, **kwargs)
|
||||
self._dump_lockfile(stages, **kwargs)
|
||||
|
||||
def dump_dataset(self, dataset):
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
datasets: list[dict] = data.setdefault("datasets", [])
|
||||
parsed = self.datasets if data else []
|
||||
raw = data.setdefault("datasets", [])
|
||||
loc = next(
|
||||
(i for i, ds in enumerate(datasets) if ds["name"] == dataset["name"]),
|
||||
(i for i, ds in enumerate(parsed) if ds["name"] == dataset["name"]),
|
||||
None,
|
||||
)
|
||||
if loc is not None:
|
||||
apply_diff(dataset, datasets[loc])
|
||||
datasets[loc] = dataset
|
||||
if raw[loc] != parsed[loc]:
|
||||
raise ParametrizedDumpError(
|
||||
"cannot update a parametrized dataset entry"
|
||||
)
|
||||
|
||||
apply_diff(dataset, raw[loc])
|
||||
raw[loc] = dataset
|
||||
else:
|
||||
datasets.append(dataset)
|
||||
raw.append(dataset)
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def _dump_lockfile(self, stage, **kwargs):
|
||||
self._lockfile.dump(stage, **kwargs)
|
||||
def _dump_lockfile(self, stages, **kwargs):
|
||||
self._lockfile.dump_stages(stages, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _check_if_parametrized(stage, action: str = "dump") -> None:
|
||||
if stage.raw_data.parametrized:
|
||||
raise ParametrizedDumpError(f"cannot {action} a parametrized {stage}")
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
self._check_if_parametrized(stage)
|
||||
stage_data = serialize.to_pipeline_file(stage)
|
||||
def _dump_pipeline_file(self, stages):
|
||||
stages = stages if isinstance(stages, list) else [stages]
|
||||
if not stages:
|
||||
return
|
||||
|
||||
for stage in stages:
|
||||
self._check_if_parametrized(stage)
|
||||
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
if not data:
|
||||
logger.info("Creating '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
existing_entry = stage.name in data["stages"]
|
||||
action = "Modifying" if existing_entry else "Adding"
|
||||
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
||||
|
||||
if existing_entry:
|
||||
orig_stage_data = data["stages"][stage.name]
|
||||
apply_diff(stage_data[stage.name], orig_stage_data)
|
||||
else:
|
||||
data["stages"].update(stage_data)
|
||||
for stage in stages:
|
||||
stage_data = serialize.to_pipeline_file(stage)
|
||||
existing_entry = stage.name in data["stages"]
|
||||
action = "Modifying" if existing_entry else "Adding"
|
||||
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
||||
if existing_entry:
|
||||
orig_stage_data = data["stages"][stage.name]
|
||||
apply_diff(stage_data[stage.name], orig_stage_data)
|
||||
else:
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
@@ -308,29 +338,29 @@ class ProjectFile(FileMixin):
|
||||
return self.LOADER(self, self.contents, self.lockfile_contents)
|
||||
|
||||
@property
|
||||
def metrics(self) -> list[str]:
|
||||
return self.contents.get("metrics", [])
|
||||
def artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
return self.resolver.resolve_artifacts()
|
||||
|
||||
@property
|
||||
def plots(self) -> Any:
|
||||
return self.contents.get("plots", {})
|
||||
def metrics(self) -> list[str]:
|
||||
return self.resolver.resolve_metrics()
|
||||
|
||||
@property
|
||||
def params(self) -> list[str]:
|
||||
return self.contents.get("params", [])
|
||||
return self.resolver.resolve_params()
|
||||
|
||||
@property
|
||||
def plots(self) -> list[Any]:
|
||||
return self.resolver.resolve_plots()
|
||||
|
||||
@property
|
||||
def datasets(self) -> list[dict[str, Any]]:
|
||||
return self.contents.get("datasets", [])
|
||||
return self.resolver.resolve_datasets()
|
||||
|
||||
@property
|
||||
def datasets_lock(self) -> list[dict[str, Any]]:
|
||||
return self.lockfile_contents.get("datasets", [])
|
||||
|
||||
@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.")
|
||||
@@ -394,9 +424,12 @@ class Lockfile(FileMixin):
|
||||
data.setdefault("stages", {})
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
def dump_stages(self, stages, **kwargs):
|
||||
if not stages:
|
||||
return
|
||||
|
||||
is_modified = False
|
||||
log_updated = False
|
||||
with modify_yaml(self.path, fs=self.repo.fs) as data:
|
||||
if not data:
|
||||
data.update({"schema": "2.0"})
|
||||
@@ -404,17 +437,24 @@ class Lockfile(FileMixin):
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
stage.name, {}
|
||||
)
|
||||
if modified:
|
||||
logger.info("Updating lock file '%s'", self.relpath)
|
||||
for stage in stages:
|
||||
stage_data = serialize.to_lockfile(stage, **kwargs)
|
||||
modified = data["stages"].get(stage.name, {}) != stage_data.get(
|
||||
stage.name, {}
|
||||
)
|
||||
if modified:
|
||||
is_modified = True
|
||||
if not log_updated:
|
||||
logger.info("Updating lock file '%s'", self.relpath)
|
||||
log_updated = True
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
if modified:
|
||||
if is_modified:
|
||||
self.repo.scm_context.track_file(self.relpath)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
self.dump_stages([stage], **kwargs)
|
||||
|
||||
def remove_stage(self, stage):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
@@ -19,3 +19,4 @@ DVC_STUDIO_URL = "DVC_STUDIO_URL"
|
||||
DVC_SQLALCHEMY_ECHO = "DVC_SQLALCHEMY_ECHO"
|
||||
DVC_SYSTEM_CONFIG_DIR = "DVC_SYSTEM_CONFIG_DIR"
|
||||
DVC_UPDATER_ENDPOINT = "DVC_UPDATER_ENDPOINT"
|
||||
DVC_STAGE = "DVC_STAGE"
|
||||
|
||||
+9
-9
@@ -1,4 +1,5 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
|
||||
import errno
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
@@ -8,7 +9,7 @@ if TYPE_CHECKING:
|
||||
from dvc.stage import Stage
|
||||
|
||||
|
||||
class DvcException(Exception):
|
||||
class DvcException(Exception): # noqa: N818
|
||||
"""Base class for all dvc exceptions."""
|
||||
|
||||
def __init__(self, msg, *args):
|
||||
@@ -132,11 +133,11 @@ class MoveNotDataSourceError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = (
|
||||
"move is not permitted for stages that are not data sources. "
|
||||
"You need to either move '{path}' to a new location and edit "
|
||||
"it by hand, or remove '{path}' and create a new one at the "
|
||||
f"You need to either move {path!r} to a new location and edit "
|
||||
f"it by hand, or remove {path!r} and create a new one at the "
|
||||
"desired location."
|
||||
)
|
||||
super().__init__(msg.format(path=path))
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class NotDvcRepoError(DvcException):
|
||||
@@ -241,11 +242,11 @@ class UploadError(FileTransferError):
|
||||
|
||||
|
||||
class CheckoutError(DvcException):
|
||||
def __init__(self, target_infos: list[str], stats: dict[str, list[str]]):
|
||||
def __init__(self, target_infos: list[str], result: dict):
|
||||
from dvc.utils import error_link
|
||||
|
||||
self.target_infos = target_infos
|
||||
self.stats = stats
|
||||
self.result = result
|
||||
targets = [str(t) for t in target_infos]
|
||||
m = (
|
||||
"Checkout failed for following targets:\n{}\nIs your "
|
||||
@@ -270,9 +271,8 @@ class NoOutputInExternalRepoError(DvcException):
|
||||
from dvc.utils import relpath
|
||||
|
||||
super().__init__(
|
||||
"Output '{}' not found in target repository '{}'".format(
|
||||
relpath(path, external_repo_path), external_repo_url
|
||||
)
|
||||
f"Output {relpath(path, external_repo_path)!r} "
|
||||
f"not found in target repository '{external_repo_url}'"
|
||||
)
|
||||
|
||||
|
||||
|
||||
+19
-8
@@ -1,10 +1,11 @@
|
||||
import glob
|
||||
from itertools import repeat
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
|
||||
from dvc.config import ConfigError as RepoConfigError
|
||||
from dvc.config_schema import SCHEMA, Invalid
|
||||
from dvc_http import HTTPFileSystem, HTTPSFileSystem # noqa: F401
|
||||
|
||||
# pylint: disable=unused-import
|
||||
from dvc_objects.fs import ( # noqa: F401
|
||||
@@ -19,7 +20,7 @@ from dvc_objects.fs import ( # noqa: F401
|
||||
system,
|
||||
utils,
|
||||
)
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TCH001
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem # noqa: F401, TC001
|
||||
from dvc_objects.fs.errors import ( # noqa: F401
|
||||
AuthError,
|
||||
ConfigError,
|
||||
@@ -47,12 +48,24 @@ known_implementations.update(
|
||||
|
||||
def download(
|
||||
fs: "FileSystem", fs_path: str, to: str, jobs: Optional[int] = None
|
||||
) -> int:
|
||||
) -> list[tuple[str, str, Optional[dict]]]:
|
||||
from dvc.scm import lfs_prefetch
|
||||
|
||||
from .callbacks import TqdmCallback
|
||||
|
||||
with TqdmCallback(desc=f"Downloading {fs.name(fs_path)}", unit="files") as cb:
|
||||
if isinstance(fs, DVCFileSystem):
|
||||
lfs_prefetch(
|
||||
fs,
|
||||
[
|
||||
f"{fs.normpath(glob.escape(fs_path))}/**"
|
||||
if fs.isdir(fs_path)
|
||||
else glob.escape(fs_path)
|
||||
],
|
||||
)
|
||||
if not glob.has_magic(fs_path):
|
||||
return fs._get(fs_path, to, batch_size=jobs, callback=cb)
|
||||
|
||||
# NOTE: We use dvc-objects generic.copy over fs.get since it makes file
|
||||
# download atomic and avoids fsspec glob/regex path expansion.
|
||||
if fs.isdir(fs_path):
|
||||
@@ -61,7 +74,7 @@ def download(
|
||||
]
|
||||
if not from_infos:
|
||||
localfs.makedirs(to, exist_ok=True)
|
||||
return 0
|
||||
return []
|
||||
to_infos = [
|
||||
localfs.join(to, *fs.relparts(info, fs_path)) for info in from_infos
|
||||
]
|
||||
@@ -69,12 +82,10 @@ def download(
|
||||
from_infos = [fs_path]
|
||||
to_infos = [to]
|
||||
|
||||
if isinstance(fs, DVCFileSystem):
|
||||
lfs_prefetch(fs, from_infos)
|
||||
cb.set_size(len(from_infos))
|
||||
jobs = jobs or fs.jobs
|
||||
generic.copy(fs, from_infos, localfs, to_infos, callback=cb, batch_size=jobs)
|
||||
return len(to_infos)
|
||||
return list(zip(from_infos, to_infos, repeat(None)))
|
||||
|
||||
|
||||
def parse_external_url(url, fs_config=None, config=None):
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
from contextlib import ExitStack
|
||||
from typing import TYPE_CHECKING, BinaryIO, Optional, Union
|
||||
|
||||
import fsspec
|
||||
from fsspec.callbacks import DEFAULT_CALLBACK, Callback # noqa: F401
|
||||
from fsspec.callbacks import TqdmCallback as _TqdmCallback
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils.objects import cached_property
|
||||
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
|
||||
from dvc.ui._rich_progress import RichTransferProgress
|
||||
|
||||
|
||||
class TqdmCallback(fsspec.callbacks.TqdmCallback):
|
||||
class TqdmCallback(_TqdmCallback):
|
||||
def __init__(
|
||||
self,
|
||||
size: Optional[int] = None,
|
||||
|
||||
+190
-16
@@ -4,22 +4,28 @@ import ntpath
|
||||
import os
|
||||
import posixpath
|
||||
import threading
|
||||
from collections import deque
|
||||
from contextlib import ExitStack, suppress
|
||||
from collections import defaultdict, deque
|
||||
from contextlib import ExitStack, nullcontext, suppress
|
||||
from glob import has_magic
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
||||
|
||||
from fsspec.spec import AbstractFileSystem
|
||||
from fsspec.spec import DEFAULT_CALLBACK, AbstractFileSystem
|
||||
from funcy import wrap_with
|
||||
|
||||
from dvc.log import logger
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
from dvc.utils.threadpool import ThreadPoolExecutor
|
||||
from dvc_objects.fs.base import AnyFSPath, FileSystem
|
||||
|
||||
from .data import DataFileSystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from contextlib import AbstractContextManager
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.types import DictStrAny, StrPath
|
||||
|
||||
from .callbacks import Callback
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
RepoFactory = Union[Callable[..., "Repo"], type["Repo"]]
|
||||
@@ -77,11 +83,10 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
cachable = False
|
||||
root_marker = "/"
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
def __init__(
|
||||
self,
|
||||
url: Optional[str] = None,
|
||||
repo: Union["Repo", os.PathLike[str], str, None] = None,
|
||||
rev: Optional[str] = None,
|
||||
repo: Optional["Repo"] = None,
|
||||
subrepos: bool = False,
|
||||
repo_factory: Optional[RepoFactory] = None,
|
||||
fo: Optional[str] = None,
|
||||
@@ -95,7 +100,8 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
"""DVC + git-tracked files fs.
|
||||
|
||||
Args:
|
||||
path (str, optional): URL or path to a DVC/Git repository.
|
||||
repo (str | os.PathLike[str] | Repo, optional): A url or a path to a DVC/Git
|
||||
repository, or a `Repo` instance.
|
||||
Defaults to a DVC repository in the current working directory.
|
||||
Both HTTP and SSH protocols are supported for remote Git repos
|
||||
(e.g. [user@]server:project.git).
|
||||
@@ -105,7 +111,6 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
In case of a local repository, if rev is unspecified, it will
|
||||
default to the working directory.
|
||||
If the repo is not a Git repo, this option is ignored.
|
||||
repo (:obj:`Repo`, optional): `Repo` instance.
|
||||
subrepos (bool): traverse to subrepos.
|
||||
By default, it ignores subrepos.
|
||||
repo_factory (callable): A function to initialize subrepo with.
|
||||
@@ -130,13 +135,23 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
... rev="main",
|
||||
... )
|
||||
"""
|
||||
from dvc.repo import Repo
|
||||
|
||||
# kwargs.get("url") is for maintaining backward compatibility
|
||||
repo = repo or fo or kwargs.get("url")
|
||||
if isinstance(repo, Repo):
|
||||
self._repo: Optional[Repo] = repo
|
||||
url = None
|
||||
else:
|
||||
self._repo = None
|
||||
url = os.fspath(repo) if repo else None
|
||||
|
||||
super().__init__()
|
||||
self._repo = repo
|
||||
self._repo_factory = repo_factory
|
||||
self._traverse_subrepos = subrepos
|
||||
self._repo_stack = ExitStack()
|
||||
self._repo_kwargs = {
|
||||
"url": url if url is not None else fo,
|
||||
"url": url,
|
||||
"rev": rev,
|
||||
"subrepos": subrepos,
|
||||
"config": config,
|
||||
@@ -360,7 +375,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
def ls(self, path, detail=True, dvc_only=False, **kwargs): # noqa: C901
|
||||
def ls(self, path, detail=True, dvc_only=False, **kwargs): # noqa: C901, PLR0912
|
||||
key = self._get_key_from_relative(path)
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
|
||||
@@ -429,7 +444,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
ignore_subrepos = kwargs.get("ignore_subrepos", True)
|
||||
return self._info(key, path, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _info( # noqa: C901, PLR0912
|
||||
def _info( # noqa: C901
|
||||
self, key, path, ignore_subrepos=True, check_ignored=True
|
||||
):
|
||||
repo, dvc_fs, subkey = self._get_subrepo_info(key)
|
||||
@@ -474,9 +489,128 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
info["name"] = path
|
||||
return info
|
||||
|
||||
def get(
|
||||
self,
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=False,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
maxdepth=None,
|
||||
batch_size=None,
|
||||
**kwargs,
|
||||
):
|
||||
self._get(
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=recursive,
|
||||
callback=callback,
|
||||
maxdepth=maxdepth,
|
||||
batch_size=batch_size,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _get( # noqa: C901, PLR0912, PLR0915
|
||||
self,
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=False,
|
||||
callback=DEFAULT_CALLBACK,
|
||||
maxdepth=None,
|
||||
batch_size=None,
|
||||
**kwargs,
|
||||
) -> list[tuple[str, str, Optional[dict]]]:
|
||||
if (
|
||||
isinstance(rpath, list)
|
||||
or isinstance(lpath, list)
|
||||
or has_magic(rpath)
|
||||
or not self.exists(rpath)
|
||||
or not recursive
|
||||
):
|
||||
super().get(
|
||||
rpath,
|
||||
lpath,
|
||||
recursive=recursive,
|
||||
callback=callback,
|
||||
maxdepth=maxdepth,
|
||||
**kwargs,
|
||||
)
|
||||
return []
|
||||
|
||||
if os.path.isdir(lpath) or lpath.endswith(os.path.sep):
|
||||
lpath = self.join(lpath, os.path.basename(rpath))
|
||||
|
||||
if self.isfile(rpath):
|
||||
with callback.branched(rpath, lpath) as child:
|
||||
self.get_file(rpath, lpath, callback=child, **kwargs)
|
||||
return [(rpath, lpath, None)]
|
||||
|
||||
result: list[tuple[str, str, Optional[dict]]] = []
|
||||
_dirs: list[str] = []
|
||||
_files: dict[FileSystem, list[tuple[str, str, Optional[dict]]]]
|
||||
_files = defaultdict(list)
|
||||
|
||||
for root, dirs, files in self.walk(rpath, maxdepth=maxdepth, detail=True):
|
||||
if files:
|
||||
callback.set_size((callback.size or 0) + len(files))
|
||||
|
||||
parts = self.relparts(root, rpath)
|
||||
if parts in ((os.curdir,), ("",)):
|
||||
parts = ()
|
||||
dest_root = os.path.join(lpath, *parts)
|
||||
if not maxdepth or len(parts) < maxdepth - 1:
|
||||
_dirs.extend(f"{dest_root}{os.path.sep}{d}" for d in dirs)
|
||||
|
||||
key = self._get_key_from_relative(root)
|
||||
_, dvc_fs, _ = self._get_subrepo_info(key)
|
||||
|
||||
for name, info in files.items():
|
||||
dvc_info = info.get("dvc_info")
|
||||
fs_info = info.get("fs_info")
|
||||
if dvc_fs and dvc_info and not fs_info:
|
||||
fs = dvc_fs
|
||||
fs_path = dvc_info["name"]
|
||||
else:
|
||||
fs = self.repo.fs
|
||||
fs_path = fs_info["name"]
|
||||
|
||||
src_path = f"{root}{self.sep}{name}"
|
||||
dest_path = f"{dest_root}{os.path.sep}{name}"
|
||||
_files[fs].append((fs_path, dest_path, dvc_info))
|
||||
result.append((src_path, dest_path, info))
|
||||
|
||||
os.makedirs(lpath, exist_ok=True)
|
||||
for d in _dirs:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
def get_file(arg: tuple[FileSystem, tuple[str, str, Optional[dict]]]):
|
||||
fs, (src, dest, info) = arg
|
||||
kw = kwargs
|
||||
if isinstance(fs, DataFileSystem):
|
||||
kw = kw | {"info": info}
|
||||
with callback.branched(src, dest) as child:
|
||||
fs.get_file(src, dest, callback=child, **kw)
|
||||
|
||||
if batch_size == 1:
|
||||
ctx: AbstractContextManager = nullcontext()
|
||||
map_fn: Callable = map
|
||||
else:
|
||||
ctx = ThreadPoolExecutor(max_workers=batch_size)
|
||||
map_fn = ctx.imap_unordered
|
||||
|
||||
with ctx:
|
||||
it = ((fs, f) for fs, files in _files.items() for f in files)
|
||||
deque(callback.wrap(map_fn(get_file, it)), maxlen=0)
|
||||
return result
|
||||
|
||||
def get_file(self, rpath, lpath, **kwargs):
|
||||
dvc_info = kwargs.pop("info", {}).pop("dvc_info", None)
|
||||
key = self._get_key_from_relative(rpath)
|
||||
fs_path = self._from_key(key)
|
||||
dirpath = os.path.dirname(lpath)
|
||||
if dirpath:
|
||||
# makedirs raises error if the string is empty
|
||||
os.makedirs(dirpath, exist_ok=True)
|
||||
|
||||
try:
|
||||
return self.repo.fs.get_file(fs_path, lpath, **kwargs)
|
||||
except FileNotFoundError:
|
||||
@@ -485,7 +619,7 @@ class _DVCFileSystem(AbstractFileSystem):
|
||||
raise
|
||||
|
||||
dvc_path = _get_dvc_path(dvc_fs, subkey)
|
||||
return dvc_fs.get_file(dvc_path, lpath, **kwargs)
|
||||
return dvc_fs.get_file(dvc_path, lpath, info=dvc_info, **kwargs)
|
||||
|
||||
def du(self, path, total=True, maxdepth=None, withdirs=False, **kwargs):
|
||||
if maxdepth is not None:
|
||||
@@ -553,6 +687,45 @@ class DVCFileSystem(FileSystem):
|
||||
def getcwd(self):
|
||||
return self.fs.getcwd()
|
||||
|
||||
def _get(
|
||||
self,
|
||||
from_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
to_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
recursive: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> list[tuple[str, str, Optional[dict]]]:
|
||||
# FileSystem.get is non-recursive by default if arguments are lists
|
||||
# otherwise, it's recursive.
|
||||
recursive = not (isinstance(from_info, list) and isinstance(to_info, list))
|
||||
return self.fs._get(
|
||||
from_info,
|
||||
to_info,
|
||||
callback=callback,
|
||||
recursive=recursive,
|
||||
batch_size=batch_size,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def get(
|
||||
self,
|
||||
from_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
to_info: Union[AnyFSPath, list[AnyFSPath]],
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
recursive: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self._get(
|
||||
from_info,
|
||||
to_info,
|
||||
callback=callback,
|
||||
batch_size=batch_size,
|
||||
recursive=recursive,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def fsid(self) -> str:
|
||||
return self.fs.fsid
|
||||
@@ -569,9 +742,10 @@ class DVCFileSystem(FileSystem):
|
||||
return self.fs.repo_url
|
||||
|
||||
def from_os_path(self, path: str) -> str:
|
||||
if os.path.isabs(path):
|
||||
if os.path.isabs(path) or (
|
||||
os.name == "nt" and posixpath.isabs(path) and ntpath.sep not in path
|
||||
):
|
||||
path = os.path.relpath(path, self.repo.root_dir)
|
||||
|
||||
return as_posix(path)
|
||||
|
||||
def close(self):
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ Examples
|
||||
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from upath import UPath
|
||||
from upath import UPath # ty: ignore[unresolved-import]
|
||||
|
||||
|
||||
class DVCPath(UPath):
|
||||
|
||||
+213
-100
@@ -1,8 +1,9 @@
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from collections.abc import Iterable, Iterator
|
||||
from itertools import chain, groupby, takewhile
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Union, overload
|
||||
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pathspec.util import normalize_file
|
||||
@@ -13,7 +14,9 @@ from dvc.log import logger
|
||||
from dvc.pathspec_math import PatternInfo, merge_patterns
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs import AnyFSPath, FileSystem
|
||||
from typing_extensions import Self
|
||||
|
||||
from dvc.fs import FileSystem
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
@@ -21,36 +24,79 @@ logger = logger.getChild(__name__)
|
||||
class DvcIgnore:
|
||||
DVCIGNORE_FILE = ".dvcignore"
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DvcIgnorePatterns(DvcIgnore):
|
||||
def __init__(self, pattern_list, dirname, sep):
|
||||
def __init__(
|
||||
self, pattern_list: Iterable[Union[PatternInfo, str]], dirname: str, sep: str
|
||||
) -> None:
|
||||
from pathspec.patterns.gitwildmatch import _DIR_MARK
|
||||
|
||||
if pattern_list and isinstance(pattern_list[0], str):
|
||||
pattern_list = [PatternInfo(pattern, "") for pattern in pattern_list]
|
||||
|
||||
self.sep = sep
|
||||
self.pattern_list = pattern_list
|
||||
self.dirname = dirname
|
||||
|
||||
self.regex_pattern_list = []
|
||||
for count, pattern in enumerate(pattern_list):
|
||||
pattern, group = GitWildMatchPattern.pattern_to_regex(pattern.patterns)
|
||||
if pattern:
|
||||
pattern = pattern.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
||||
self.regex_pattern_list.append((pattern, group))
|
||||
|
||||
self.ignore_spec = [
|
||||
(ignore, re.compile("|".join(item[0] for item in group)))
|
||||
for ignore, group in groupby(self.regex_pattern_list, lambda x: x[1])
|
||||
if ignore is not None
|
||||
pattern_infos = [
|
||||
pattern if isinstance(pattern, PatternInfo) else PatternInfo(pattern, "")
|
||||
for pattern in pattern_list
|
||||
]
|
||||
|
||||
self.sep = sep
|
||||
self.pattern_list: list[PatternInfo] = []
|
||||
self.dirname = dirname
|
||||
self.find_matching_pattern = functools.cache(self._find_matching_pattern)
|
||||
|
||||
regex_pattern_list: list[tuple[str, bool, bool, PatternInfo]] = []
|
||||
for count, pattern_info in enumerate(pattern_infos):
|
||||
regex, ignore = GitWildMatchPattern.pattern_to_regex(pattern_info.patterns)
|
||||
if regex is not None and ignore is not None:
|
||||
self.pattern_list.append(pattern_info)
|
||||
regex = regex.replace(f"<{_DIR_MARK}>", f"<{_DIR_MARK}{count}>")
|
||||
regex_pattern_list.append(
|
||||
(regex, ignore, pattern_info.patterns.endswith("/"), pattern_info)
|
||||
)
|
||||
|
||||
def keyfunc(item: tuple[str, bool, bool, PatternInfo]) -> tuple[bool, bool]:
|
||||
_, ignore, dir_only_pattern, _ = item
|
||||
return ignore, dir_only_pattern
|
||||
|
||||
self.ignore_spec: list[
|
||||
tuple[
|
||||
re.Pattern[str],
|
||||
bool,
|
||||
bool,
|
||||
dict[Optional[str], tuple[str, PatternInfo]],
|
||||
]
|
||||
]
|
||||
self.ignore_spec = []
|
||||
for (ignore, dir_only_pattern), group in groupby(
|
||||
regex_pattern_list, key=keyfunc
|
||||
):
|
||||
if ignore:
|
||||
# For performance, we combine all exclude patterns.
|
||||
# But we still need to figure out which pattern matched which rule,
|
||||
# (eg: to show in `dvc check-ignore`).
|
||||
# So, we use named groups and keep a map of group name to pattern.
|
||||
pattern_map: dict[Optional[str], tuple[str, PatternInfo]] = {
|
||||
f"rule_{i}": (regex, pi)
|
||||
for i, (regex, _, _, pi) in enumerate(group)
|
||||
}
|
||||
combined_regex = "|".join(
|
||||
f"(?P<{name}>{regex})" for name, (regex, _) in pattern_map.items()
|
||||
)
|
||||
self.ignore_spec.append(
|
||||
(re.compile(combined_regex), ignore, dir_only_pattern, pattern_map)
|
||||
)
|
||||
else:
|
||||
# unignored patterns are not combined with `|`.
|
||||
for regex, _, _, pi in group:
|
||||
pattern_map = {None: (regex, pi)}
|
||||
self.ignore_spec.append(
|
||||
(re.compile(regex), ignore, dir_only_pattern, pattern_map)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path, fs, name):
|
||||
def from_file(cls, path: str, fs: "FileSystem", name: str) -> "Self":
|
||||
assert fs.isabs(path)
|
||||
dirname = fs.normpath(fs.dirname(path))
|
||||
with fs.open(path, encoding="utf-8") as fobj:
|
||||
@@ -62,13 +108,15 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
|
||||
return cls(path_spec_lines, dirname, fs.sep)
|
||||
|
||||
def __call__(self, root: list[str], dirs: list[str], files: list[str]):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
files = [f for f in files if not self.matches(root, f)]
|
||||
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
||||
|
||||
return dirs, files
|
||||
|
||||
def _get_normalize_path(self, dirname, basename):
|
||||
def _get_normalize_path(self, dirname: str, basename: str) -> Optional[str]:
|
||||
# NOTE: `relpath` is too slow, so we have to assume that both
|
||||
# `dirname` and `self.dirname` are relative or absolute together.
|
||||
|
||||
@@ -81,62 +129,105 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
# NOTE: `os.path.join` is ~x5.5 slower
|
||||
path = f"{rel}{self.sep}{basename}"
|
||||
else:
|
||||
return False
|
||||
return None
|
||||
|
||||
if os.name == "nt":
|
||||
return normalize_file(path)
|
||||
return path
|
||||
|
||||
def matches(self, dirname, basename, is_dir=False, details: bool = False):
|
||||
@overload
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: Literal[False] = ...,
|
||||
) -> bool: ...
|
||||
|
||||
@overload
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: Literal[True] = ...,
|
||||
) -> tuple[bool, list[PatternInfo]]: ...
|
||||
|
||||
@overload
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: bool = False,
|
||||
) -> Union[bool, tuple[bool, list[PatternInfo]]]: ...
|
||||
|
||||
def matches(
|
||||
self,
|
||||
dirname: str,
|
||||
basename: str,
|
||||
is_dir: bool = False,
|
||||
details: bool = False,
|
||||
) -> Union[bool, tuple[bool, list[PatternInfo]]]:
|
||||
path = self._get_normalize_path(dirname, basename)
|
||||
if not path:
|
||||
return False
|
||||
|
||||
if details:
|
||||
return self._ignore_details(path, is_dir)
|
||||
return self.ignore(path, is_dir)
|
||||
|
||||
def ignore(self, path, is_dir):
|
||||
def matches(pattern, path, is_dir) -> bool:
|
||||
matches_ = bool(pattern.match(path))
|
||||
|
||||
if is_dir:
|
||||
matches_ |= bool(pattern.match(f"{path}/"))
|
||||
|
||||
return matches_
|
||||
|
||||
result = False
|
||||
_match: list[PatternInfo] = []
|
||||
if path:
|
||||
result, _match = self._ignore(path, is_dir)
|
||||
return (result, _match) if details else result
|
||||
|
||||
for ignore, pattern in self.ignore_spec[::-1]:
|
||||
if matches(pattern, path, is_dir):
|
||||
result = ignore
|
||||
break
|
||||
return result
|
||||
def _find_matching_pattern(
|
||||
self, path: str, is_dir: bool
|
||||
) -> tuple[bool, list[PatternInfo]]:
|
||||
paths = [path]
|
||||
if is_dir and not path.endswith("/"):
|
||||
paths.append(f"{path}/")
|
||||
|
||||
def _ignore_details(self, path, is_dir: bool):
|
||||
result = []
|
||||
for (regex, _), pattern_info in list(
|
||||
zip(self.regex_pattern_list, self.pattern_list)
|
||||
for pattern, ignore, dir_only_pattern, pattern_map in reversed(
|
||||
self.ignore_spec
|
||||
):
|
||||
# skip system pattern
|
||||
if not pattern_info.file_info:
|
||||
if dir_only_pattern and not is_dir:
|
||||
continue
|
||||
for p in paths:
|
||||
match = pattern.match(p)
|
||||
if not match:
|
||||
continue
|
||||
if ignore:
|
||||
group_name, _match = next(
|
||||
(
|
||||
(name, _match)
|
||||
for name, _match in match.groupdict().items()
|
||||
if name.startswith("rule_") and _match is not None
|
||||
)
|
||||
)
|
||||
else:
|
||||
# unignored patterns are not combined with `|`,
|
||||
# so there are no groups.
|
||||
group_name = None
|
||||
_regex, pattern_info = pattern_map[group_name]
|
||||
return ignore, [pattern_info]
|
||||
return False, []
|
||||
|
||||
regex = re.compile(regex)
|
||||
def _ignore(self, path: str, is_dir: bool) -> tuple[bool, list[PatternInfo]]:
|
||||
parts = path.split("/")
|
||||
result = False
|
||||
matches: list[PatternInfo] = []
|
||||
for i in range(1, len(parts) + 1):
|
||||
rel_path = "/".join(parts[:i])
|
||||
result, _matches = self.find_matching_pattern(
|
||||
rel_path, is_dir or i < len(parts)
|
||||
)
|
||||
if i < len(parts) and not result:
|
||||
continue
|
||||
matches.extend(_matches)
|
||||
if result:
|
||||
break
|
||||
return result, matches
|
||||
|
||||
matches = bool(regex.match(path))
|
||||
if is_dir:
|
||||
matches |= bool(regex.match(f"{path}/"))
|
||||
|
||||
if matches:
|
||||
result.append(pattern_info.file_info)
|
||||
|
||||
return result
|
||||
|
||||
def __hash__(self):
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.dirname + ":" + str(self.pattern_list))
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, DvcIgnorePatterns):
|
||||
return NotImplemented
|
||||
return (self.dirname == other.dirname) & (
|
||||
@@ -144,21 +235,18 @@ class DvcIgnorePatterns(DvcIgnore):
|
||||
== [pattern.patterns for pattern in other.pattern_list]
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.pattern_list)
|
||||
|
||||
|
||||
CheckIgnoreResult = namedtuple( # noqa: PYI024
|
||||
"CheckIgnoreResult", ["file", "match", "patterns"]
|
||||
)
|
||||
|
||||
|
||||
def _no_match(path):
|
||||
return CheckIgnoreResult(path, False, ["::"])
|
||||
class CheckIgnoreResult(NamedTuple):
|
||||
file: str
|
||||
match: bool
|
||||
pattern_infos: list[PatternInfo]
|
||||
|
||||
|
||||
class DvcIgnoreFilter:
|
||||
def __init__(self, fs, root_dir):
|
||||
def __init__(self, fs: "FileSystem", root_dir: str) -> None:
|
||||
from dvc.repo import Repo
|
||||
|
||||
default_ignore_patterns = [
|
||||
@@ -193,7 +281,7 @@ class DvcIgnoreFilter:
|
||||
ignore_subrepos=True,
|
||||
)
|
||||
|
||||
def _get_key(self, path):
|
||||
def _get_key(self, path: str) -> tuple[str, ...]:
|
||||
parts = self.fs.relparts(path, self.root_dir)
|
||||
if parts == (os.curdir,):
|
||||
return ()
|
||||
@@ -241,7 +329,7 @@ class DvcIgnoreFilter:
|
||||
for dname in dnames:
|
||||
self._update_sub_repo(self.fs.join(dirname, dname), ignore_trie)
|
||||
|
||||
def _update_sub_repo(self, path, ignore_trie: Trie):
|
||||
def _update_sub_repo(self, path: str, ignore_trie: Trie) -> None:
|
||||
from dvc.repo import Repo
|
||||
|
||||
if path == self.root_dir:
|
||||
@@ -268,7 +356,9 @@ class DvcIgnoreFilter:
|
||||
else:
|
||||
ignore_trie[key] = new_pattern
|
||||
|
||||
def __call__(self, root, dirs, files, ignore_subrepos=True):
|
||||
def __call__(
|
||||
self, root: str, dirs: list[str], files: list[str], ignore_subrepos: bool = True
|
||||
) -> tuple[list[str], list[str]]:
|
||||
abs_root = self.fs.abspath(root)
|
||||
ignore_pattern = self._get_trie_pattern(
|
||||
abs_root, dnames=dirs, ignore_subrepos=ignore_subrepos
|
||||
@@ -277,7 +367,24 @@ class DvcIgnoreFilter:
|
||||
dirs, files = ignore_pattern(abs_root, dirs, files)
|
||||
return dirs, files
|
||||
|
||||
def ls(self, fs, path, detail=True, **kwargs):
|
||||
@overload
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: Literal[True], **kwargs: Any
|
||||
) -> list[dict[str, Any]]: ...
|
||||
|
||||
@overload
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: Literal[False], **kwargs
|
||||
) -> list[str]: ...
|
||||
|
||||
@overload
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs
|
||||
) -> Union[list[str], list[dict[str, Any]]]: ...
|
||||
|
||||
def ls(
|
||||
self, fs: "FileSystem", path: str, detail: bool = True, **kwargs: Any
|
||||
) -> Union[list[str], list[dict[str, Any]]]:
|
||||
fs_dict = {}
|
||||
dirs = []
|
||||
nondirs = []
|
||||
@@ -297,22 +404,29 @@ class DvcIgnoreFilter:
|
||||
|
||||
return [fs_dict[name] for name in chain(dirs, nondirs)]
|
||||
|
||||
def walk(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
|
||||
def walk(
|
||||
self, fs: "FileSystem", path: str, **kwargs: Any
|
||||
) -> Iterator[
|
||||
Union[
|
||||
tuple[str, list[str], list[str]],
|
||||
tuple[str, dict[str, dict], dict[str, dict]],
|
||||
]
|
||||
]:
|
||||
detail = kwargs.get("detail", False)
|
||||
ignore_subrepos = kwargs.pop("ignore_subrepos", True)
|
||||
if fs.protocol == Schemes.LOCAL:
|
||||
for root, dirs, files in fs.walk(path, **kwargs):
|
||||
if detail:
|
||||
all_dnames = set(dirs.keys())
|
||||
all_fnames = set(files.keys())
|
||||
assert isinstance(dirs, dict)
|
||||
assert isinstance(files, dict)
|
||||
dnames, fnames = self(
|
||||
root,
|
||||
all_dnames,
|
||||
all_fnames,
|
||||
list(dirs),
|
||||
list(files),
|
||||
ignore_subrepos=ignore_subrepos,
|
||||
)
|
||||
list(map(dirs.pop, all_dnames - set(dnames)))
|
||||
list(map(files.pop, all_fnames - set(fnames)))
|
||||
list(map(dirs.pop, dirs.keys() - set(dnames)))
|
||||
list(map(files.pop, files.keys() - set(fnames)))
|
||||
else:
|
||||
dirs[:], files[:] = self(
|
||||
root, dirs, files, ignore_subrepos=ignore_subrepos
|
||||
@@ -321,7 +435,7 @@ class DvcIgnoreFilter:
|
||||
else:
|
||||
yield from fs.walk(path, **kwargs)
|
||||
|
||||
def find(self, fs: "FileSystem", path: "AnyFSPath", **kwargs):
|
||||
def find(self, fs: "FileSystem", path: str, **kwargs: Any) -> Iterator[str]:
|
||||
if fs.protocol == Schemes.LOCAL:
|
||||
for root, _, files in self.walk(fs, path, **kwargs):
|
||||
for file in files:
|
||||
@@ -331,7 +445,7 @@ class DvcIgnoreFilter:
|
||||
yield from fs.find(path)
|
||||
|
||||
def _get_trie_pattern(
|
||||
self, dirname, dnames: Optional["list"] = None, ignore_subrepos=True
|
||||
self, dirname: str, dnames: Optional[list[str]] = None, ignore_subrepos=True
|
||||
) -> Optional["DvcIgnorePatterns"]:
|
||||
if ignore_subrepos:
|
||||
ignores_trie = self.ignores_trie_fs
|
||||
@@ -367,7 +481,7 @@ class DvcIgnoreFilter:
|
||||
|
||||
def _is_ignored(
|
||||
self, path: str, is_dir: bool = False, ignore_subrepos: bool = True
|
||||
):
|
||||
) -> bool:
|
||||
if self._outside_repo(path):
|
||||
return False
|
||||
dirname, basename = self.fs.split(self.fs.normpath(path))
|
||||
@@ -389,24 +503,23 @@ class DvcIgnoreFilter:
|
||||
path = self.fs.abspath(path)
|
||||
return self._is_ignored(path, False, ignore_subrepos=ignore_subrepos)
|
||||
|
||||
def _outside_repo(self, path):
|
||||
def _outside_repo(self, path: str) -> bool:
|
||||
return not self.fs.isin_or_eq(path, self.root_dir)
|
||||
|
||||
def check_ignore(self, target):
|
||||
def check_ignore(self, target: str) -> CheckIgnoreResult:
|
||||
# NOTE: can only be used in `dvc check-ignore`, see
|
||||
# https://github.com/iterative/dvc/issues/5046
|
||||
full_target = self.fs.abspath(target)
|
||||
matched_patterns: list[PatternInfo] = []
|
||||
ignore = False
|
||||
if not self._outside_repo(full_target):
|
||||
dirname, basename = self.fs.split(self.fs.normpath(full_target))
|
||||
pattern = self._get_trie_pattern(dirname)
|
||||
if pattern:
|
||||
matches = pattern.matches(
|
||||
dirname, basename, self.fs.isdir(full_target), True
|
||||
ignore, matched_patterns = pattern.matches(
|
||||
dirname, basename, self.fs.isdir(full_target), details=True
|
||||
)
|
||||
|
||||
if matches:
|
||||
return CheckIgnoreResult(target, True, matches)
|
||||
return _no_match(target)
|
||||
return CheckIgnoreResult(target, ignore, matched_patterns)
|
||||
|
||||
def is_ignored(
|
||||
self, fs: "FileSystem", path: str, ignore_subrepos: bool = True
|
||||
@@ -424,7 +537,7 @@ class DvcIgnoreFilter:
|
||||
)
|
||||
|
||||
|
||||
def init(path):
|
||||
def init(path: Union[str, os.PathLike[str]]) -> str:
|
||||
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
||||
if os.path.exists(dvcignore):
|
||||
return dvcignore
|
||||
@@ -439,7 +552,7 @@ def init(path):
|
||||
return dvcignore
|
||||
|
||||
|
||||
def destroy(path):
|
||||
def destroy(path: Union[str, os.PathLike[str]]) -> None:
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
dvcignore = os.path.join(path, DvcIgnore.DVCIGNORE_FILE)
|
||||
|
||||
+7
-6
@@ -48,7 +48,10 @@ def get_dvc_info():
|
||||
info.append("Cache types: " + error_link("no-dvc-cache"))
|
||||
|
||||
info.append(f"Caches: {_get_caches(repo.cache)}")
|
||||
info.append(f"Remotes: {_get_remotes(repo.config)}")
|
||||
|
||||
configured_remotes = _get_remotes(repo.config)
|
||||
remotes = ", ".join(configured_remotes) if configured_remotes else None
|
||||
info.append(f"Remotes: {remotes}")
|
||||
|
||||
root_directory = repo.root_dir
|
||||
fs_root = _get_fs_type(os.path.abspath(root_directory))
|
||||
@@ -75,12 +78,10 @@ def _get_caches(cache):
|
||||
|
||||
|
||||
def _get_remotes(config):
|
||||
schemes = (
|
||||
return [
|
||||
get_fs_cls(get_fs_config(config, name=remote)).protocol
|
||||
for remote in config["remote"]
|
||||
)
|
||||
|
||||
return ", ".join(schemes) or "None"
|
||||
]
|
||||
|
||||
|
||||
def _get_linktype_support_info(repo):
|
||||
@@ -123,7 +124,7 @@ def _get_supported_remotes():
|
||||
if not fs_cls.get_missing_deps():
|
||||
dependencies = []
|
||||
for requirement in fs_cls.REQUIRES:
|
||||
dependencies.append(
|
||||
dependencies.append( # noqa: PERF401
|
||||
f"{requirement} = {importlib_metadata.version(requirement)}"
|
||||
)
|
||||
|
||||
|
||||
+47
-20
@@ -2,12 +2,13 @@
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Union
|
||||
|
||||
import flufl.lock
|
||||
import zc.lockfile
|
||||
from funcy import retry
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.progress import Tqdm
|
||||
@@ -87,9 +88,10 @@ class Lock(LockBase):
|
||||
Uses zc.lockfile as backend.
|
||||
"""
|
||||
|
||||
def __init__(self, lockfile, friendly=False, **kwargs):
|
||||
def __init__(self, lockfile, friendly=False, wait=False, **kwargs):
|
||||
super().__init__(lockfile)
|
||||
self._friendly = friendly
|
||||
self._wait = wait
|
||||
self._lock = None
|
||||
self._lock_failed = False
|
||||
|
||||
@@ -100,23 +102,38 @@ class Lock(LockBase):
|
||||
def _do_lock(self):
|
||||
try:
|
||||
self._lock_failed = False
|
||||
with Tqdm(
|
||||
bar_format="{desc}",
|
||||
disable=not self._friendly,
|
||||
desc="If DVC froze, see `hardlink_lock` in {}".format(
|
||||
format_link("https://man.dvc.org/config#core")
|
||||
),
|
||||
):
|
||||
self._lock = zc.lockfile.LockFile(self._lockfile)
|
||||
self._lock = zc.lockfile.LockFile(self._lockfile)
|
||||
except zc.lockfile.LockError:
|
||||
self._lock_failed = True
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
||||
|
||||
def lock(self):
|
||||
retries = 6
|
||||
delay = DEFAULT_TIMEOUT / retries
|
||||
lock_retry = retry(retries, LockError, timeout=delay)(self._do_lock)
|
||||
lock_retry()
|
||||
"""Acquire the lock, either waiting forever, or after default_retries."""
|
||||
default_retries = 6
|
||||
delay = DEFAULT_TIMEOUT / default_retries
|
||||
attempts = 0
|
||||
|
||||
max_retries = float("inf") if self._wait else default_retries
|
||||
|
||||
with Tqdm(
|
||||
bar_format="{desc}",
|
||||
disable=not self._friendly,
|
||||
desc="Waiting to acquire lock. "
|
||||
"If DVC froze, see `hardlink_lock` in {}.".format(
|
||||
format_link("https://man.dvc.org/config#core")
|
||||
),
|
||||
) as pbar:
|
||||
while True:
|
||||
try:
|
||||
self._do_lock()
|
||||
return
|
||||
except LockError:
|
||||
attempts += 1
|
||||
if attempts > max_retries:
|
||||
raise
|
||||
time.sleep(delay)
|
||||
finally:
|
||||
pbar.update()
|
||||
|
||||
def unlock(self):
|
||||
if self._lock_failed:
|
||||
@@ -149,7 +166,7 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
tmp_dir (str): a directory to store claim files.
|
||||
"""
|
||||
|
||||
def __init__(self, lockfile, tmp_dir=None, **kwargs):
|
||||
def __init__(self, lockfile, tmp_dir=None, wait=False, **kwargs):
|
||||
import socket
|
||||
|
||||
self._tmp_dir = tmp_dir
|
||||
@@ -172,11 +189,21 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
self._separator = flufl.lock.SEP
|
||||
self._set_claimfile()
|
||||
self._owned = True
|
||||
self._wait = wait
|
||||
self._retry_errnos = []
|
||||
self._friendly = kwargs.get("friendly", False)
|
||||
|
||||
def lock(self):
|
||||
def lock(self, timeout: Optional[Union[timedelta, int]] = None):
|
||||
try:
|
||||
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
|
||||
if not self._wait:
|
||||
timeout = timeout or timedelta(seconds=DEFAULT_TIMEOUT)
|
||||
|
||||
with Tqdm(
|
||||
bar_format="{desc}",
|
||||
disable=not (self._wait and self._friendly),
|
||||
desc="Waiting to acquire lock",
|
||||
):
|
||||
super().lock(timeout)
|
||||
except flufl.lock.TimeOutError:
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
|
||||
|
||||
@@ -185,11 +212,11 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
|
||||
if self._tmp_dir is not None:
|
||||
# Under Windows file path length is limited so we hash it
|
||||
hasher = hashlib.md5(self._claimfile.encode()) # noqa: S324
|
||||
hasher = hashlib.md5(self._claimfile.encode(), usedforsecurity=False)
|
||||
filename = hasher.hexdigest()
|
||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||
|
||||
|
||||
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
|
||||
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False, wait=False):
|
||||
cls = HardlinkLock if hardlink_lock else Lock
|
||||
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly)
|
||||
return cls(lockfile, tmp_dir=tmp_dir, friendly=friendly, wait=wait)
|
||||
|
||||
+16
-4
@@ -5,6 +5,8 @@ import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from typing import ClassVar
|
||||
|
||||
import colorama
|
||||
@@ -36,7 +38,7 @@ def add_logging_level(level_name, level_num, method_name=None):
|
||||
self._log(level_num, message, args, **kwargs)
|
||||
|
||||
def log_to_root(message, *args, **kwargs):
|
||||
logging.log(level_num, message, *args, **kwargs)
|
||||
logging.log(level_num, message, *args, **kwargs) # noqa: LOG015
|
||||
|
||||
# getLevelName resolves the numeric log level if already defined,
|
||||
# otherwise returns a string
|
||||
@@ -53,7 +55,7 @@ def add_logging_level(level_name, level_num, method_name=None):
|
||||
setattr(logging, method_name, log_to_root)
|
||||
|
||||
|
||||
class LoggingException(Exception):
|
||||
class LoggingException(Exception): # noqa: N818
|
||||
def __init__(self, record):
|
||||
msg = f"failed to log {record!s}"
|
||||
super().__init__(msg)
|
||||
@@ -174,9 +176,19 @@ def _iter_causes(exc):
|
||||
exc = exc.__cause__
|
||||
|
||||
|
||||
def set_loggers_level(level: int = logging.INFO) -> None:
|
||||
@contextmanager
|
||||
def set_loggers_level(level: int = logging.INFO) -> Iterator[None]:
|
||||
ret: dict[logging.Logger, int] = {}
|
||||
for name in ["dvc", "dvc_objects", "dvc_data"]:
|
||||
logging.getLogger(name).setLevel(level)
|
||||
_logger = logging.getLogger(name)
|
||||
ret[_logger] = _logger.getEffectiveLevel()
|
||||
_logger.setLevel(level)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
for _logger, old_lvl in ret.items():
|
||||
_logger.setLevel(old_lvl)
|
||||
|
||||
|
||||
def setup(level: int = logging.INFO, log_colors: bool = True) -> None:
|
||||
|
||||
+54
-29
@@ -45,7 +45,7 @@ if TYPE_CHECKING:
|
||||
from dvc_data.hashfile.obj import HashFile
|
||||
from dvc_data.index import DataIndexKey
|
||||
|
||||
from .ignore import DvcIgnoreFilter
|
||||
from .ignore import CheckIgnoreResult, DvcIgnoreFilter
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
@@ -267,9 +267,9 @@ class OutputIsStageFileError(DvcException):
|
||||
|
||||
|
||||
class OutputIsIgnoredError(DvcException):
|
||||
def __init__(self, match):
|
||||
lines = "\n".join(match.patterns)
|
||||
super().__init__(f"Path '{match.file}' is ignored by\n{lines}")
|
||||
def __init__(self, result: "CheckIgnoreResult"):
|
||||
pi = result.pattern_infos[-1]
|
||||
super().__init__(f"Path {result.file!r} is ignored by {pi!s}")
|
||||
|
||||
|
||||
class CheckoutCallback(TqdmCallback):
|
||||
@@ -390,7 +390,7 @@ class Output:
|
||||
self.can_push = push
|
||||
|
||||
self.fs_path = self._parse_path(self.fs, fs_path)
|
||||
self.obj: Optional["HashFile"] = None
|
||||
self.obj: Optional[HashFile] = None
|
||||
|
||||
self.remote = remote
|
||||
|
||||
@@ -431,7 +431,7 @@ class Output:
|
||||
self.meta.nfiles = len(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:
|
||||
elif self.meta.nfiles or (self.hash_info and self.hash_info.isdir):
|
||||
self.meta.isdir = True
|
||||
if not self.hash_info and self.hash_name not in ("md5", "md5-dos2unix"):
|
||||
md5 = getattr(self.meta, "md5", None)
|
||||
@@ -542,7 +542,9 @@ class Output:
|
||||
desc=f"Collecting files and computing hashes in {self}",
|
||||
disable=no_progress_bar,
|
||||
) as pb:
|
||||
return build(*args, callback=pb.as_callback(), **kwargs)
|
||||
kwargs["callback"] = pb.as_callback()
|
||||
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
|
||||
return build(*args, **kwargs)
|
||||
|
||||
def _get_hash_meta(self):
|
||||
if self.use_cache:
|
||||
@@ -730,6 +732,7 @@ class Output:
|
||||
from dvc_data.hashfile.checkout import LinkError, PromptError
|
||||
|
||||
kwargs.setdefault("ignore", self.dvcignore)
|
||||
kwargs.setdefault("checksum_jobs", self.fs.hash_jobs)
|
||||
try:
|
||||
return checkout(*args, **kwargs)
|
||||
except PromptError as exc:
|
||||
@@ -749,11 +752,9 @@ 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
|
||||
hardlink = relink and next(iter(self.cache.cache_types), None) == "hardlink"
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info, hardlink)
|
||||
obj = self._commit_granular_dir(filter_info, hardlink=hardlink)
|
||||
else:
|
||||
staging, _, obj = self._build(
|
||||
self.cache,
|
||||
@@ -786,6 +787,7 @@ class Output:
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=cb,
|
||||
old=obj,
|
||||
)
|
||||
self.set_exec()
|
||||
|
||||
@@ -902,7 +904,7 @@ class Output:
|
||||
def get_obj(
|
||||
self, filter_info: Optional[str] = None, **kwargs
|
||||
) -> Optional["HashFile"]:
|
||||
obj: Optional["HashFile"] = None
|
||||
obj: Optional[HashFile] = None
|
||||
if self.obj:
|
||||
obj = self.obj
|
||||
elif self.files:
|
||||
@@ -1158,7 +1160,7 @@ class Output:
|
||||
logger.warning(msg)
|
||||
return {}
|
||||
|
||||
obj: Optional["HashFile"]
|
||||
obj: Optional[HashFile]
|
||||
if self.is_dir_checksum:
|
||||
obj = self._collect_used_dir_cache(**kwargs)
|
||||
else:
|
||||
@@ -1201,8 +1203,9 @@ class Output:
|
||||
if stage:
|
||||
abs_path = os.path.join(stage.wdir, path)
|
||||
if self._is_path_dvcignore(abs_path):
|
||||
check = stage.repo.dvcignore.check_ignore(abs_path)
|
||||
raise self.IsIgnoredError(check)
|
||||
check: CheckIgnoreResult = stage.repo.dvcignore.check_ignore(abs_path)
|
||||
if check.match:
|
||||
raise self.IsIgnoredError(check)
|
||||
|
||||
def _check_can_merge(self, out):
|
||||
if self.protocol != out.protocol:
|
||||
@@ -1353,7 +1356,7 @@ class Output:
|
||||
cache = self.cache if self.use_cache else self.local_cache
|
||||
assert isinstance(cache, HashFileDB)
|
||||
|
||||
new: "HashFile"
|
||||
new: HashFile
|
||||
try:
|
||||
assert self.hash_name
|
||||
staging, meta, obj = self._build(
|
||||
@@ -1365,7 +1368,7 @@ class Output:
|
||||
dry_run=not self.use_cache,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
if self.fs_path == path:
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self) from exc
|
||||
if not self.is_dir_checksum:
|
||||
raise
|
||||
@@ -1398,12 +1401,14 @@ class Output:
|
||||
|
||||
assert staging
|
||||
assert obj.hash_info
|
||||
|
||||
hardlink = relink and next(iter(self.cache.cache_types), None) == "hardlink"
|
||||
with TqdmCallback(desc=f"Adding {self} to cache", unit="file") as cb:
|
||||
otransfer(
|
||||
staging,
|
||||
self.cache,
|
||||
{obj.hash_info},
|
||||
hardlink=relink,
|
||||
hardlink=hardlink,
|
||||
shallow=False,
|
||||
callback=cb,
|
||||
)
|
||||
@@ -1421,6 +1426,7 @@ class Output:
|
||||
state=self.repo.state,
|
||||
prompt=prompt.confirm,
|
||||
progress_callback=callback,
|
||||
old=obj,
|
||||
)
|
||||
self.set_exec()
|
||||
return obj
|
||||
@@ -1447,25 +1453,44 @@ class Output:
|
||||
self.remote = other.remote
|
||||
self.can_push = other.can_push
|
||||
|
||||
def merge_version_meta(self, other: "Output"):
|
||||
def _get_versioned_meta(
|
||||
self,
|
||||
) -> Optional[
|
||||
tuple["HashInfo", Optional["Meta"], Optional[Union["HashFile", "Tree"]]]
|
||||
]:
|
||||
if self.files is not None or (
|
||||
self.meta is not None and self.meta.version_id is not None
|
||||
):
|
||||
old_obj = self.obj if self.obj is not None else self.get_obj()
|
||||
return self.hash_info, self.meta, old_obj
|
||||
return None
|
||||
|
||||
def merge_version_meta(
|
||||
self,
|
||||
old_hi: "HashInfo",
|
||||
old_meta: Optional["Meta"],
|
||||
old_obj: Optional[Union["HashFile", "Tree"]],
|
||||
):
|
||||
"""Merge version meta for files which are unchanged from other."""
|
||||
if not self.hash_info:
|
||||
return
|
||||
return None
|
||||
if self.hash_info.isdir:
|
||||
return self._merge_dir_version_meta(other)
|
||||
if self.hash_info != other.hash_info:
|
||||
return
|
||||
self.meta = other.meta
|
||||
return self._merge_dir_version_meta(old_hi, old_obj)
|
||||
if self.hash_info != old_hi:
|
||||
return None
|
||||
self.meta = old_meta
|
||||
return None
|
||||
|
||||
def _merge_dir_version_meta(self, other: "Output"):
|
||||
def _merge_dir_version_meta(
|
||||
self, old_hi: "HashInfo", old_obj: Optional[Union["HashFile", "Tree"]]
|
||||
):
|
||||
from dvc_data.hashfile.tree import update_meta
|
||||
|
||||
if not self.obj or not other.hash_info.isdir:
|
||||
if not self.obj or not old_hi.isdir:
|
||||
return
|
||||
other_obj = other.obj if other.obj is not None else other.get_obj()
|
||||
assert isinstance(self.obj, Tree)
|
||||
assert isinstance(other_obj, Tree)
|
||||
updated = update_meta(self.obj, other_obj)
|
||||
assert isinstance(old_obj, Tree)
|
||||
updated = update_meta(self.obj, old_obj)
|
||||
assert updated.hash_info == self.obj.hash_info
|
||||
self.obj = updated
|
||||
self.files = updated.as_list(with_meta=True)
|
||||
|
||||
+92
-3
@@ -21,6 +21,7 @@ from .context import (
|
||||
VarsAlreadyLoaded,
|
||||
)
|
||||
from .interpolate import (
|
||||
check_expression,
|
||||
check_recursive_parse_errors,
|
||||
is_interpolated_string,
|
||||
recurse,
|
||||
@@ -38,10 +39,16 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
STAGES_KWD = "stages"
|
||||
VARS_KWD = "vars"
|
||||
WDIR_KWD = "wdir"
|
||||
|
||||
ARTIFACTS_KWD = "artifacts"
|
||||
DATASETS_KWD = "datasets"
|
||||
METRICS_KWD = "metrics"
|
||||
PARAMS_KWD = "params"
|
||||
PLOTS_KWD = "plots"
|
||||
STAGES_KWD = "stages"
|
||||
|
||||
FOREACH_KWD = "foreach"
|
||||
MATRIX_KWD = "matrix"
|
||||
DO_KWD = "do"
|
||||
@@ -163,6 +170,27 @@ class DataResolver:
|
||||
for name, definition in stages_data.items()
|
||||
}
|
||||
|
||||
self.artifacts = [
|
||||
ArtifactDefinition(self, self.context, name, definition, ARTIFACTS_KWD)
|
||||
for name, definition in d.get(ARTIFACTS_KWD, {}).items()
|
||||
]
|
||||
self.datasets = [
|
||||
TopDefinition(self, self.context, str(i), definition, DATASETS_KWD)
|
||||
for i, definition in enumerate(d.get(DATASETS_KWD, []))
|
||||
]
|
||||
self.metrics = [
|
||||
TopDefinition(self, self.context, str(i), definition, METRICS_KWD)
|
||||
for i, definition in enumerate(d.get(METRICS_KWD, []))
|
||||
]
|
||||
self.params = [
|
||||
TopDefinition(self, self.context, str(i), definition, PARAMS_KWD)
|
||||
for i, definition in enumerate(d.get(PARAMS_KWD, []))
|
||||
]
|
||||
self.plots = [
|
||||
TopDefinition(self, self.context, str(i), definition, PLOTS_KWD)
|
||||
for i, definition in enumerate(d.get(PLOTS_KWD, []))
|
||||
]
|
||||
|
||||
def resolve_one(self, name: str):
|
||||
group, key = split_group_name(name)
|
||||
|
||||
@@ -186,6 +214,27 @@ class DataResolver:
|
||||
logger.trace("Resolved dvc.yaml:\n%s", data)
|
||||
return {STAGES_KWD: data}
|
||||
|
||||
# Top-level sections are eagerly evaluated, whereas stages are lazily evaluated,
|
||||
# one-by-one.
|
||||
|
||||
def resolve_artifacts(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
d: dict[str, Optional[dict[str, Any]]] = {}
|
||||
for item in self.artifacts:
|
||||
d.update(item.resolve())
|
||||
return d
|
||||
|
||||
def resolve_datasets(self) -> list[dict[str, Any]]:
|
||||
return [item.resolve() for item in self.datasets]
|
||||
|
||||
def resolve_metrics(self) -> list[str]:
|
||||
return [item.resolve() for item in self.metrics]
|
||||
|
||||
def resolve_params(self) -> list[str]:
|
||||
return [item.resolve() for item in self.params]
|
||||
|
||||
def resolve_plots(self) -> list[Any]:
|
||||
return [item.resolve() for item in self.plots]
|
||||
|
||||
def has_key(self, key: str):
|
||||
return self._has_group_and_key(*split_group_name(key))
|
||||
|
||||
@@ -514,10 +563,10 @@ class MatrixDefinition:
|
||||
iterable = self.resolved_iterable
|
||||
assert isinstance(iterable, Mapping)
|
||||
|
||||
ret: dict[str, "DictStrAny"] = {}
|
||||
ret: dict[str, DictStrAny] = {}
|
||||
matrix = {key: enumerate(v) for key, v in iterable.items()}
|
||||
for combination in product(*matrix.values()):
|
||||
d: "DictStrAny" = {}
|
||||
d: DictStrAny = {}
|
||||
fragments: list[str] = []
|
||||
for k, (i, v) in zip(matrix.keys(), combination):
|
||||
d[k] = v
|
||||
@@ -565,3 +614,43 @@ class MatrixDefinition:
|
||||
return entry.resolve_stage(skip_checks=True)
|
||||
except ContextError as exc:
|
||||
format_and_raise(exc, f"stage '{generated}'", self.relpath)
|
||||
|
||||
|
||||
class TopDefinition:
|
||||
def __init__(
|
||||
self,
|
||||
resolver: DataResolver,
|
||||
context: Context,
|
||||
name: str,
|
||||
definition: "Any",
|
||||
where: str,
|
||||
):
|
||||
self.resolver = resolver
|
||||
self.context = context
|
||||
self.name = name
|
||||
self.definition = definition
|
||||
self.where = where
|
||||
self.relpath = self.resolver.relpath
|
||||
|
||||
def resolve(self):
|
||||
try:
|
||||
check_recursive_parse_errors(self.definition)
|
||||
return self.context.resolve(self.definition)
|
||||
except (ParseError, ContextError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
|
||||
|
||||
|
||||
class ArtifactDefinition(TopDefinition):
|
||||
def resolve(self) -> dict[str, Optional[dict[str, Any]]]:
|
||||
try:
|
||||
check_expression(self.name)
|
||||
name = self.context.resolve(self.name)
|
||||
if not isinstance(name, str):
|
||||
typ = type(name).__name__
|
||||
raise ResolveError(
|
||||
f"failed to resolve '{self.where}.{self.name}'"
|
||||
f" in '{self.relpath}': expected str, got " + typ
|
||||
)
|
||||
except (ParseError, ContextError) as exc:
|
||||
format_and_raise(exc, f"'{self.where}.{self.name}'", self.relpath)
|
||||
return {name: super().resolve()}
|
||||
|
||||
@@ -66,7 +66,7 @@ class ParamsLoadError(ContextError):
|
||||
pass
|
||||
|
||||
|
||||
class KeyNotInContext(ContextError, KeyError):
|
||||
class KeyNotInContext(ContextError, KeyError): # noqa: N818
|
||||
def __init__(self, key: str) -> None:
|
||||
self.key: str = key
|
||||
super().__init__(f"Could not find '{key}'")
|
||||
@@ -75,7 +75,7 @@ class KeyNotInContext(ContextError, KeyError):
|
||||
return self.msg
|
||||
|
||||
|
||||
class VarsAlreadyLoaded(ContextError):
|
||||
class VarsAlreadyLoaded(ContextError): # noqa: N818
|
||||
pass
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class Meta:
|
||||
|
||||
@staticmethod
|
||||
def update_path(meta: "Meta", path: Union[str, int]):
|
||||
dpaths = meta.dpaths[:] + [str(path)]
|
||||
dpaths = [*meta.dpaths, str(path)]
|
||||
return replace(meta, dpaths=dpaths)
|
||||
|
||||
def __str__(self):
|
||||
@@ -155,7 +155,7 @@ class Value(Node):
|
||||
PRIMITIVES = (int, float, str, bytes, bool)
|
||||
|
||||
|
||||
class Container(Node, ABC):
|
||||
class Container(Node, ABC): # noqa: PLW1641
|
||||
meta: Meta
|
||||
data: Union[list, dict]
|
||||
_key_transform = staticmethod(identity)
|
||||
@@ -176,8 +176,9 @@ class Container(Node, ABC):
|
||||
return value
|
||||
if isinstance(value, (list, dict)):
|
||||
assert meta
|
||||
container = CtxDict if isinstance(value, dict) else CtxList
|
||||
return container(value, meta=meta)
|
||||
if isinstance(value, dict):
|
||||
return CtxDict(value, meta=meta)
|
||||
return CtxList(value, meta=meta)
|
||||
msg = f"Unsupported value of type '{type(value).__name__}' in '{meta}'"
|
||||
raise TypeError(msg)
|
||||
|
||||
@@ -271,7 +272,7 @@ class CtxDict(Container, MutableMapping):
|
||||
if not isinstance(key, str):
|
||||
# limitation for the interpolation
|
||||
# ignore other kinds of keys
|
||||
return
|
||||
return None
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def merge_update(self, other, overwrite=False):
|
||||
|
||||
@@ -202,6 +202,7 @@ def validate_value(value, key):
|
||||
if isinstance(value, dict) and key == "cmd":
|
||||
return True
|
||||
raise ParseError(f"Cannot interpolate data of type '{type(value).__name__}'")
|
||||
return None
|
||||
|
||||
|
||||
def str_interpolate(
|
||||
|
||||
+10
-6
@@ -2,13 +2,19 @@
|
||||
# Including changing base dir of path specification patterns and merging
|
||||
# of two path specification patterns with different base
|
||||
# All the operations follow the documents of `gitignore`
|
||||
from collections import namedtuple
|
||||
from typing import NamedTuple
|
||||
|
||||
from pathspec.util import normalize_file
|
||||
|
||||
from dvc.utils import relpath
|
||||
|
||||
PatternInfo = namedtuple("PatternInfo", ["patterns", "file_info"]) # noqa: PYI024
|
||||
|
||||
class PatternInfo(NamedTuple):
|
||||
patterns: str
|
||||
file_info: str
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.file_info or f":{self.patterns}"
|
||||
|
||||
|
||||
def _not_ignore(rule):
|
||||
@@ -27,11 +33,9 @@ def _remove_slash(rule):
|
||||
|
||||
def _match_all_level(rule):
|
||||
if rule[:-1].find("/") >= 0 and not rule.startswith("**/"):
|
||||
if rule.startswith("/"):
|
||||
rule = rule[1:]
|
||||
rule = rule.removeprefix("/")
|
||||
return False, rule
|
||||
if rule.startswith("**/"):
|
||||
rule = rule[3:]
|
||||
rule = rule.removeprefix("**/")
|
||||
return True, rule
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
"""Manages progress bars for DVC repo."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from threading import RLock
|
||||
@@ -40,7 +41,7 @@ class Tqdm(tqdm):
|
||||
"miniters": 1,
|
||||
}
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
def __init__(
|
||||
self,
|
||||
iterable=None,
|
||||
disable=None,
|
||||
|
||||
@@ -23,7 +23,7 @@ def _lists(blob: Union[dict, list]) -> Iterable[list]:
|
||||
if isinstance(blob, list):
|
||||
yield blob
|
||||
else:
|
||||
for _, value in blob.items():
|
||||
for value in blob.values():
|
||||
if isinstance(value, dict):
|
||||
yield from _lists(value)
|
||||
elif isinstance(value, list):
|
||||
@@ -167,7 +167,7 @@ class VegaConverter(Converter):
|
||||
if isinstance(y, list):
|
||||
return "y"
|
||||
if not isinstance(y, dict):
|
||||
return
|
||||
return None
|
||||
|
||||
fields = {field for _, field in _file_field(y)}
|
||||
if len(fields) == 1:
|
||||
|
||||
+20
-15
@@ -144,10 +144,11 @@ class Repo:
|
||||
scm: Optional[Union["Git", "NoSCM"]] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional["DictStrAny"] = None,
|
||||
_wait_for_lock: bool = False,
|
||||
):
|
||||
from dvc.cachemgr import CacheManager
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem, localfs
|
||||
from dvc.fs import GitFileSystem, LocalFileSystem
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.repo.artifacts import Artifacts
|
||||
from dvc.repo.datasets import Datasets
|
||||
@@ -161,12 +162,13 @@ class Repo:
|
||||
|
||||
self.url = url
|
||||
self._fs_conf = {"repo_factory": repo_factory}
|
||||
self._fs = fs or localfs
|
||||
self._fs = fs or LocalFileSystem()
|
||||
self._scm = scm
|
||||
self._config = config
|
||||
self._remote = remote
|
||||
self._remote_config = remote_config
|
||||
self._data_index = None
|
||||
self._data_index: Optional[DataIndex] = None
|
||||
self._wait_for_lock = _wait_for_lock
|
||||
|
||||
if rev and not fs:
|
||||
self._scm = scm = SCM(root_dir or os.curdir)
|
||||
@@ -184,12 +186,12 @@ class Repo:
|
||||
# used by DVCFileSystem to determine if it should traverse subrepos
|
||||
self.subrepos = subrepos
|
||||
|
||||
self.cloud: "DataCloud" = DataCloud(self)
|
||||
self.stage: "StageLoad" = StageLoad(self)
|
||||
self.cloud: DataCloud = DataCloud(self)
|
||||
self.stage: StageLoad = StageLoad(self)
|
||||
|
||||
self.lock: "LockBase"
|
||||
self.lock: LockBase
|
||||
self.cache: CacheManager
|
||||
self.state: "StateBase"
|
||||
self.state: StateBase
|
||||
if isinstance(self.fs, GitFileSystem) or not self.dvc_dir:
|
||||
self.lock = LockNoop()
|
||||
self.state = StateNoop()
|
||||
@@ -204,8 +206,14 @@ class Repo:
|
||||
tmp_dir=self.tmp_dir,
|
||||
hardlink_lock=self.config["core"].get("hardlink_lock", False),
|
||||
friendly=True,
|
||||
wait=self._wait_for_lock,
|
||||
)
|
||||
os.makedirs(self.site_cache_dir, exist_ok=True)
|
||||
if not fs and (
|
||||
checksum_jobs := self.config["core"].get("checksum_jobs")
|
||||
):
|
||||
self.fs.hash_jobs = checksum_jobs
|
||||
|
||||
self.state = State(self.root_dir, self.site_cache_dir, self.dvcignore)
|
||||
else:
|
||||
self.lock = LockNoop()
|
||||
@@ -551,11 +559,7 @@ class Repo:
|
||||
|
||||
if out.protocol == "local" and match(fs_path, out.fs_path):
|
||||
return True
|
||||
|
||||
if recursive and out.fs.isin(out.fs_path, fs_path):
|
||||
return True
|
||||
|
||||
return False
|
||||
return recursive and out.fs.isin(out.fs_path, fs_path)
|
||||
|
||||
matched = list(filter(func, outs))
|
||||
if not matched:
|
||||
@@ -608,7 +612,7 @@ class Repo:
|
||||
from dvc.fs import GitFileSystem
|
||||
from dvc.version import version_tuple
|
||||
|
||||
cache_dir = self.config["core"].get("site_cache_dir") or site_cache_dir()
|
||||
cache_dir = site_cache_dir(self.config["core"].get("site_cache_dir"))
|
||||
|
||||
subdir = None
|
||||
if isinstance(self.fs, GitFileSystem):
|
||||
@@ -637,10 +641,11 @@ class Repo:
|
||||
# that just happened to be at the same path as old deleted ones.
|
||||
btime = self._btime or getattr(os.stat(root_dir), "st_birthtime", None)
|
||||
|
||||
md5 = hashlib.md5( # noqa: S324
|
||||
md5 = hashlib.md5(
|
||||
str(
|
||||
(root_dir, subdir, btime, getpass.getuser(), version_tuple[0], salt)
|
||||
).encode()
|
||||
).encode(),
|
||||
usedforsecurity=False,
|
||||
)
|
||||
repo_token = md5.hexdigest()
|
||||
return os.path.join(repos_dir, repo_token)
|
||||
|
||||
+14
-3
@@ -171,11 +171,16 @@ def _add_transfer(
|
||||
stage.dump()
|
||||
|
||||
|
||||
def _add(stage: "Stage", source: Optional[str] = None, no_commit: bool = False) -> None:
|
||||
def _add(
|
||||
stage: "Stage",
|
||||
source: Optional[str] = None,
|
||||
no_commit: bool = False,
|
||||
relink: bool = True,
|
||||
) -> None:
|
||||
out = stage.outs[0]
|
||||
path = out.fs.abspath(source) if source else None
|
||||
try:
|
||||
stage.add_outs(path, no_commit=no_commit)
|
||||
stage.add_outs(path, no_commit=no_commit, relink=relink)
|
||||
except CacheLinkError:
|
||||
stage.dump()
|
||||
raise
|
||||
@@ -194,6 +199,7 @@ def add(
|
||||
to_remote: bool = False,
|
||||
remote_jobs: Optional[int] = None,
|
||||
force: bool = False,
|
||||
relink: bool = True,
|
||||
) -> list["Stage"]:
|
||||
add_targets = find_targets(targets, glob=glob)
|
||||
if not add_targets:
|
||||
@@ -224,7 +230,12 @@ def add(
|
||||
with warn_link_failures() as link_failures:
|
||||
for source, (stage, output_exists) in progress_iter(stages_with_targets):
|
||||
try:
|
||||
_add(stage, source if output_exists else None, no_commit=no_commit)
|
||||
_add(
|
||||
stage,
|
||||
source if output_exists else None,
|
||||
no_commit=no_commit,
|
||||
relink=relink,
|
||||
)
|
||||
except CacheLinkError:
|
||||
link_failures.append(stage.relpath)
|
||||
return stages
|
||||
|
||||
+66
-24
@@ -12,7 +12,7 @@ from dvc.exceptions import (
|
||||
InvalidArgumentError,
|
||||
)
|
||||
from dvc.log import logger
|
||||
from dvc.utils import relpath, resolve_output
|
||||
from dvc.utils import as_posix, relpath, resolve_output
|
||||
from dvc.utils.objects import cached_property
|
||||
from dvc.utils.serialize import modify_yaml
|
||||
|
||||
@@ -99,7 +99,7 @@ class Artifacts:
|
||||
"""Read artifacts from dvc.yaml."""
|
||||
artifacts: dict[str, dict[str, Artifact]] = {}
|
||||
for dvcfile, dvcfile_artifacts in self.repo.index._artifacts.items():
|
||||
dvcyaml = relpath(dvcfile, self.repo.root_dir)
|
||||
dvcyaml = self.repo.fs.relpath(dvcfile, self.repo.root_dir)
|
||||
artifacts[dvcyaml] = {}
|
||||
for name, value in dvcfile_artifacts.items():
|
||||
try:
|
||||
@@ -137,18 +137,16 @@ class Artifacts:
|
||||
|
||||
assert not (version and stage)
|
||||
name = _reformat_name(name)
|
||||
tags: list["GitTag"] = find(
|
||||
name=name, version=version, stage=stage, scm=self.scm
|
||||
)
|
||||
tags: list[GitTag] = find(name=name, version=version, stage=stage, scm=self.scm)
|
||||
if not tags:
|
||||
raise ArtifactNotFoundError(name, version=version, stage=stage)
|
||||
if version or stage:
|
||||
return tags[-1].target
|
||||
gto_tags: list["GTOTag"] = sort_versions(parse_tag(tag) for tag in tags)
|
||||
gto_tags: list[GTOTag] = sort_versions(parse_tag(tag) for tag in tags)
|
||||
return gto_tags[0].tag.target
|
||||
|
||||
def get_path(self, name: str):
|
||||
"""Return repo fspath for the given artifact."""
|
||||
@classmethod
|
||||
def parse_path(cls, name: str) -> tuple[Optional[str], str]:
|
||||
from gto.constants import SEPARATOR_IN_NAME, fullname_re
|
||||
|
||||
name = _reformat_name(name)
|
||||
@@ -158,13 +156,37 @@ class Artifacts:
|
||||
dirname = m.group("dirname")
|
||||
if dirname:
|
||||
dirname = dirname.rstrip(SEPARATOR_IN_NAME)
|
||||
dvcyaml = os.path.join(dirname, PROJECT_FILE) if dirname else PROJECT_FILE
|
||||
artifact_name = m.group("name")
|
||||
|
||||
return dirname, m.group("name")
|
||||
|
||||
def get_path(self, name: str):
|
||||
"""Return fspath for the given artifact relative to the git root."""
|
||||
from dvc.fs import GitFileSystem
|
||||
|
||||
dirname, artifact_name = self.parse_path(name)
|
||||
# `name`/`dirname` are expected to be a git root relative.
|
||||
# We convert it to dvc-root relative path so that we can read artifacts
|
||||
# from dvc.yaml file.
|
||||
# But we return dirname intact, as we want to return a git-root relative path.
|
||||
# This is useful when reading from `dvcfs` from remote.
|
||||
fs = self.repo.fs
|
||||
assert self.scm
|
||||
if isinstance(fs, GitFileSystem):
|
||||
scm_root = fs.root_marker
|
||||
else:
|
||||
scm_root = self.scm.root_dir
|
||||
|
||||
dirparts = posixpath.normpath(dirname).split(posixpath.sep) if dirname else ()
|
||||
abspath = fs.join(scm_root, *dirparts, PROJECT_FILE)
|
||||
rela = fs.relpath(abspath, self.repo.root_dir)
|
||||
try:
|
||||
artifact = self.read()[dvcyaml][artifact_name]
|
||||
artifact = self.read()[rela][artifact_name]
|
||||
except KeyError as exc:
|
||||
raise ArtifactNotFoundError(name) from exc
|
||||
return os.path.join(dirname, artifact.path) if dirname else artifact.path
|
||||
|
||||
path = posixpath.join(dirname or "", artifact.path)
|
||||
parts = posixpath.normpath(path).split(posixpath.sep)
|
||||
return os.path.join(*parts)
|
||||
|
||||
def download(
|
||||
self,
|
||||
@@ -177,15 +199,28 @@ class Artifacts:
|
||||
) -> tuple[int, str]:
|
||||
"""Download the specified artifact."""
|
||||
from dvc.fs import download as fs_download
|
||||
from dvc.repo import Repo
|
||||
|
||||
logger.debug("Trying to download artifact '%s' via DVC", name)
|
||||
rev = self.get_rev(name, version=version, stage=stage)
|
||||
|
||||
dirname, _ = self.parse_path(name)
|
||||
with self.repo.switch(rev):
|
||||
path = self.get_path(name)
|
||||
root = self.repo.fs.root_marker
|
||||
_dirname = self.repo.fs.join(root, dirname) if dirname else root
|
||||
with Repo(_dirname, fs=self.repo.fs, scm=self.repo.scm) as r:
|
||||
path = r.artifacts.get_path(name)
|
||||
path = self.repo.fs.join(root, as_posix(path))
|
||||
path = self.repo.fs.relpath(path, self.repo.root_dir)
|
||||
# when the `repo` is a subrepo, the path `/subrepo/myart.pkl` for dvcfs
|
||||
# should be translated as `/myart.pkl`,
|
||||
# i.e. relative to the root of the subrepo
|
||||
path = self.repo.fs.join(root, path)
|
||||
path = self.repo.fs.normpath(path)
|
||||
|
||||
out = resolve_output(path, out, force=force)
|
||||
fs = self.repo.dvcfs
|
||||
fs_path = fs.from_os_path(path)
|
||||
count = fs_download(fs, fs_path, os.path.abspath(out), jobs=jobs)
|
||||
count = len(fs_download(fs, path, os.path.abspath(out), jobs=jobs))
|
||||
return count, out
|
||||
|
||||
@staticmethod
|
||||
@@ -200,10 +235,9 @@ class Artifacts:
|
||||
dvc_studio_config: Optional[dict[str, Any]] = None,
|
||||
**kwargs,
|
||||
) -> tuple[int, str]:
|
||||
from dvc_studio_client.model_registry import get_download_uris
|
||||
|
||||
from dvc.fs import HTTPFileSystem, generic, localfs
|
||||
from dvc.fs.callbacks import TqdmCallback
|
||||
from dvc_studio_client.model_registry import get_download_uris
|
||||
|
||||
logger.debug("Trying to download artifact '%s' via studio", name)
|
||||
out = out or os.getcwd()
|
||||
@@ -231,7 +265,7 @@ class Artifacts:
|
||||
from_infos.append(url)
|
||||
except DvcException:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
raise DvcException(
|
||||
f"Failed to download artifact '{name}' via Studio"
|
||||
) from exc
|
||||
@@ -249,7 +283,7 @@ class Artifacts:
|
||||
return len(to_infos), relpath(localfs.commonpath(to_infos))
|
||||
|
||||
@classmethod
|
||||
def get( # noqa: PLR0913
|
||||
def get(
|
||||
cls,
|
||||
url: str,
|
||||
name: str,
|
||||
@@ -278,6 +312,13 @@ class Artifacts:
|
||||
|
||||
name = _reformat_name(name)
|
||||
saved_exc: Optional[Exception] = None
|
||||
|
||||
local_dvc_studio_config = Config().get("studio", {})
|
||||
args_dvc_studio_config = {}
|
||||
if config and not isinstance(config, dict):
|
||||
config = Config.load_file(config)
|
||||
args_dvc_studio_config = config.get("studio", {})
|
||||
|
||||
try:
|
||||
logger.trace("Trying studio-only config")
|
||||
return cls._download_studio(
|
||||
@@ -288,14 +329,13 @@ class Artifacts:
|
||||
out=out,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
dvc_studio_config=local_dvc_studio_config | args_dvc_studio_config,
|
||||
)
|
||||
except FileExistsLocallyError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
saved_exc = exc
|
||||
|
||||
if config and not isinstance(config, dict):
|
||||
config = Config.load_file(config)
|
||||
with Repo.open(
|
||||
url=url,
|
||||
subrepos=True,
|
||||
@@ -305,7 +345,7 @@ class Artifacts:
|
||||
remote_config=remote_config,
|
||||
) as repo:
|
||||
logger.trace("Trying repo [studio] config")
|
||||
dvc_studio_config = dict(repo.config.get("studio"))
|
||||
repo_dvc_studio_config = repo.config.get("studio", {})
|
||||
try:
|
||||
return cls._download_studio(
|
||||
url,
|
||||
@@ -315,7 +355,9 @@ class Artifacts:
|
||||
out=out,
|
||||
force=force,
|
||||
jobs=jobs,
|
||||
dvc_studio_config=dvc_studio_config,
|
||||
dvc_studio_config=local_dvc_studio_config
|
||||
| repo_dvc_studio_config
|
||||
| args_dvc_studio_config,
|
||||
)
|
||||
except FileExistsLocallyError:
|
||||
raise
|
||||
@@ -333,7 +375,7 @@ class Artifacts:
|
||||
)
|
||||
except FileExistsLocallyError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
except Exception as exc:
|
||||
if saved_exc:
|
||||
logger.exception(str(saved_exc), exc_info=saved_exc.__cause__)
|
||||
raise DvcException(
|
||||
|
||||
+53
-25
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc.exceptions import (
|
||||
@@ -14,7 +15,9 @@ from dvc.utils import relpath
|
||||
from . import locked
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc_data.index import BaseDataIndex, DataIndexEntry
|
||||
from dvc.repo.index import IndexView
|
||||
from dvc_data.index import BaseDataIndex, DataIndexEntry, DataIndexKey
|
||||
from dvc_data.index.diff import Change
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
@@ -36,10 +39,12 @@ def _remove_unused_links(repo):
|
||||
return ret
|
||||
|
||||
|
||||
def _build_out_changes(index, changes):
|
||||
def _build_out_changes(
|
||||
index: "IndexView", changes: dict["DataIndexKey", "Change"]
|
||||
) -> dict["DataIndexKey", tuple[str, dict[str, int]]]:
|
||||
from dvc_data.index.checkout import MODIFY
|
||||
|
||||
out_keys = []
|
||||
out_keys: list[DataIndexKey] = []
|
||||
for out in index.outs:
|
||||
if not out.use_cache:
|
||||
continue
|
||||
@@ -47,19 +52,28 @@ def _build_out_changes(index, changes):
|
||||
ws, key = out.index_key
|
||||
if ws != "repo":
|
||||
continue
|
||||
|
||||
out_keys.append(key)
|
||||
|
||||
out_changes = {}
|
||||
out_stats: dict[DataIndexKey, dict[str, int]]
|
||||
out_stats = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
out_changes: dict[DataIndexKey, tuple[str, dict[str, int]]] = {}
|
||||
for key, change in changes.items():
|
||||
typ = change.typ
|
||||
isdir = change.new and change.new.isdir
|
||||
for out_key in out_keys:
|
||||
if len(out_key) > len(key) or key[: len(out_key)] != out_key:
|
||||
continue
|
||||
|
||||
stats = out_stats[out_key]
|
||||
if not isdir:
|
||||
stats[typ] += 1
|
||||
|
||||
if key == out_key:
|
||||
out_changes[out_key] = change.typ
|
||||
elif not out_changes.get(out_key):
|
||||
out_changes[out_key] = MODIFY
|
||||
out_changes[out_key] = typ, stats
|
||||
elif out_key not in out_changes:
|
||||
typ = MODIFY
|
||||
out_changes[out_key] = typ, stats
|
||||
break
|
||||
|
||||
return out_changes
|
||||
@@ -107,14 +121,13 @@ def checkout( # noqa: C901
|
||||
from dvc.stage.exceptions import StageFileBadNameError, StageFileDoesNotExistError
|
||||
from dvc_data.index.checkout import ADD, DELETE, MODIFY, apply, compare
|
||||
|
||||
stats: dict[str, list[str]] = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
}
|
||||
stats = {"modified": 0, "added": 0, "deleted": 0}
|
||||
changes: dict[str, list[str]] = {"modified": [], "added": [], "deleted": []}
|
||||
|
||||
if not targets:
|
||||
targets = [None]
|
||||
stats["deleted"] = _remove_unused_links(self)
|
||||
changes["deleted"] = _remove_unused_links(self)
|
||||
stats["deleted"] = len(changes["deleted"])
|
||||
|
||||
if isinstance(targets, str):
|
||||
targets = [targets]
|
||||
@@ -125,10 +138,12 @@ def checkout( # noqa: C901
|
||||
(StageFileDoesNotExistError, StageFileBadNameError, NoOutputOrStageError),
|
||||
):
|
||||
raise CheckoutErrorSuggestGit(target) from exc
|
||||
raise
|
||||
raise # noqa: PLE0704
|
||||
|
||||
view = self.index.targets_view(
|
||||
targets, recursive=recursive, with_deps=with_deps, onerror=onerror
|
||||
from .index import index_from_targets
|
||||
|
||||
view = index_from_targets(
|
||||
self, targets=targets, recursive=recursive, with_deps=with_deps, onerror=onerror
|
||||
)
|
||||
|
||||
with ui.progress(unit="entry", desc="Building workspace index", leave=True) as pb:
|
||||
@@ -149,7 +164,10 @@ def checkout( # noqa: C901
|
||||
|
||||
def checkout_onerror(src_path, dest_path, _exc):
|
||||
logger.debug(
|
||||
"failed to create '%s' from '%s'", dest_path, src_path, exc_info=True
|
||||
"failed to create '%s' from '%s'",
|
||||
dest_path,
|
||||
src_path,
|
||||
exc_info=True, # noqa: LOG014
|
||||
)
|
||||
|
||||
for out_path in out_paths:
|
||||
@@ -171,16 +189,26 @@ def checkout( # noqa: C901
|
||||
out_changes = _build_out_changes(view, diff.changes)
|
||||
|
||||
typ_map = {ADD: "added", DELETE: "deleted", MODIFY: "modified"}
|
||||
for key, typ in out_changes.items():
|
||||
for key, (typ, _stats) in out_changes.items():
|
||||
out_path = self.fs.join(self.root_dir, *key)
|
||||
|
||||
if out_path in failed:
|
||||
self.fs.remove(out_path, recursive=True)
|
||||
else:
|
||||
self.state.save_link(out_path, self.fs)
|
||||
stats[typ_map[typ]].append(_fspath_dir(out_path))
|
||||
continue
|
||||
|
||||
self.state.save_link(out_path, self.fs)
|
||||
for t, count in _stats.items():
|
||||
stats_typ = typ_map[t]
|
||||
stats[stats_typ] += count
|
||||
|
||||
changes[typ_map[typ]].append(_fspath_dir(out_path))
|
||||
|
||||
for changelist in changes.values():
|
||||
# group directories first, then files. But keep them alphabetically sorted
|
||||
changelist.sort(key=lambda p: (not p.endswith(os.sep), p))
|
||||
|
||||
result = changes | {"stats": stats}
|
||||
if failed and not allow_missing:
|
||||
raise CheckoutError([relpath(out_path) for out_path in failed], stats)
|
||||
|
||||
return stats
|
||||
result["failed"] = [relpath(out_path) for out_path in failed]
|
||||
raise CheckoutError([relpath(out_path) for out_path in failed], result)
|
||||
return result
|
||||
|
||||
+3
-3
@@ -43,13 +43,13 @@ def _collect_paths(
|
||||
|
||||
|
||||
def _filter_outs(
|
||||
outs: Outputs, fs_paths: StrPaths, duplicates=False
|
||||
repo: "Repo", outs: Outputs, fs_paths: StrPaths, duplicates=False
|
||||
) -> tuple[Outputs, StrPaths]:
|
||||
res_outs: Outputs = []
|
||||
fs_res_paths = fs_paths
|
||||
|
||||
for out in outs:
|
||||
fs_path = out.repo.dvcfs.from_os_path(out.fs_path)
|
||||
fs_path = repo.dvcfs.from_os_path(out.fs_path)
|
||||
if fs_path in fs_paths:
|
||||
res_outs.append(out)
|
||||
if not duplicates:
|
||||
@@ -77,4 +77,4 @@ def collect(
|
||||
|
||||
target_paths = _collect_paths(repo, targets, recursive=recursive)
|
||||
|
||||
return _filter_outs(outs, target_paths, duplicates=duplicates)
|
||||
return _filter_outs(repo, outs, target_paths, duplicates=duplicates)
|
||||
|
||||
+32
-22
@@ -1,3 +1,4 @@
|
||||
from itertools import groupby
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dvc import prompt
|
||||
@@ -52,29 +53,38 @@ def commit(
|
||||
data_only=False,
|
||||
relink=True,
|
||||
):
|
||||
stages_info = [
|
||||
info
|
||||
for info in self.stage.collect_granular(
|
||||
target, with_deps=with_deps, recursive=recursive
|
||||
)
|
||||
if not data_only or info.stage.is_data_source
|
||||
]
|
||||
for stage_info in stages_info:
|
||||
stage = stage_info.stage
|
||||
if force:
|
||||
stage.save(allow_missing=allow_missing)
|
||||
else:
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
committed_stages = []
|
||||
groups = groupby(
|
||||
[
|
||||
info
|
||||
for info in self.stage.collect_granular(
|
||||
target, with_deps=with_deps, recursive=recursive
|
||||
)
|
||||
if not data_only or info.stage.is_data_source
|
||||
],
|
||||
key=lambda info: info.stage.dvcfile,
|
||||
)
|
||||
|
||||
for dvcfile, stages_info_group in groups:
|
||||
to_dump = []
|
||||
for stage_info in stages_info_group:
|
||||
stage = stage_info.stage
|
||||
if force:
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info,
|
||||
allow_missing=allow_missing,
|
||||
relink=relink,
|
||||
)
|
||||
stage.dump(update_pipeline=False)
|
||||
return [s.stage for s in stages_info]
|
||||
else:
|
||||
changes = stage.changed_entries()
|
||||
if any(changes):
|
||||
prompt_to_commit(stage, changes, force=force)
|
||||
stage.save(allow_missing=allow_missing)
|
||||
stage.commit(
|
||||
filter_info=stage_info.filter_info,
|
||||
allow_missing=allow_missing,
|
||||
relink=relink,
|
||||
)
|
||||
to_dump.append(stage)
|
||||
dvcfile.dump_stages(to_dump, update_pipeline=False)
|
||||
committed_stages.extend(to_dump)
|
||||
return committed_stages
|
||||
|
||||
|
||||
@locked
|
||||
|
||||
+377
-78
@@ -1,38 +1,34 @@
|
||||
import os
|
||||
import posixpath
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING, Any, TypedDict, Union
|
||||
from collections import defaultdict
|
||||
from collections.abc import Iterable, Iterator, Mapping
|
||||
from typing import TYPE_CHECKING, Optional, TypedDict, Union
|
||||
|
||||
from dvc.fs.callbacks import DEFAULT_CALLBACK
|
||||
from dvc.fs.callbacks import DEFAULT_CALLBACK, Callback, TqdmCallback
|
||||
from dvc.log import logger
|
||||
from dvc.ui import ui
|
||||
from dvc_data.index import DataIndexDirError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.callbacks import Callback
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import Git, NoSCM
|
||||
from dvc_data.index import DataIndex
|
||||
from dvc_data.index import (
|
||||
BaseDataIndex,
|
||||
DataIndex,
|
||||
DataIndexEntry,
|
||||
DataIndexKey,
|
||||
DataIndexView,
|
||||
)
|
||||
from dvc_data.index.diff import Change
|
||||
from dvc_objects.fs.base import FileSystem
|
||||
|
||||
logger = logger.getChild(__name__)
|
||||
|
||||
|
||||
def posixpath_to_os_path(path: str) -> str:
|
||||
return path.replace(posixpath.sep, os.path.sep)
|
||||
|
||||
|
||||
def _adapt_typ(typ: str) -> str:
|
||||
from dvc_data.index.diff import ADD, DELETE, MODIFY
|
||||
|
||||
if typ == MODIFY:
|
||||
return "modified"
|
||||
|
||||
if typ == ADD:
|
||||
return "added"
|
||||
|
||||
if typ == DELETE:
|
||||
return "deleted"
|
||||
|
||||
return typ
|
||||
|
||||
|
||||
def _adapt_path(change: "Change") -> str:
|
||||
isdir = False
|
||||
if change.new and change.new.meta:
|
||||
@@ -45,27 +41,98 @@ def _adapt_path(change: "Change") -> str:
|
||||
return os.path.sep.join(key)
|
||||
|
||||
|
||||
def _adapt_path_from_entry(entry: "DataIndexEntry") -> str:
|
||||
key = entry.key
|
||||
assert key
|
||||
if entry.meta and entry.meta.isdir:
|
||||
key = (*key, "")
|
||||
return os.sep.join(key)
|
||||
|
||||
|
||||
def _get_missing_paths(
|
||||
to_check: Mapping["FileSystem", Mapping[str, Iterable["DataIndexEntry"]]],
|
||||
batch_size: Optional[int] = None,
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
) -> Iterator[str]:
|
||||
for fs, paths_map in to_check.items():
|
||||
if batch_size == 1 or (batch_size is None and fs.protocol == "local"):
|
||||
results = list(callback.wrap(map(fs.exists, paths_map)))
|
||||
else:
|
||||
results = fs.exists(
|
||||
list(paths_map), batch_size=batch_size, callback=callback
|
||||
)
|
||||
|
||||
for cache_path, exists in zip(paths_map, results):
|
||||
if exists:
|
||||
continue
|
||||
|
||||
for entry in paths_map[cache_path]:
|
||||
yield _adapt_path_from_entry(entry)
|
||||
|
||||
|
||||
class StorageCallback(Callback):
|
||||
def __init__(self, parent_cb: Callback) -> None:
|
||||
super().__init__(size=0, value=0)
|
||||
self.parent_cb = parent_cb
|
||||
|
||||
def set_size(self, size: int) -> None:
|
||||
# This is a no-op to prevent `fs.exists` from trying to set the size
|
||||
pass
|
||||
|
||||
def relative_update(self, value: int = 1) -> None:
|
||||
self.parent_cb.relative_update(value)
|
||||
|
||||
def absolute_update(self, value: int) -> None:
|
||||
self.parent_cb.relative_update(value - self.value)
|
||||
|
||||
|
||||
class Rename(TypedDict):
|
||||
old: str
|
||||
new: str
|
||||
|
||||
|
||||
class DiffResult(TypedDict, total=False):
|
||||
modified: list[str]
|
||||
added: list[str]
|
||||
deleted: list[str]
|
||||
renamed: list[Rename]
|
||||
unchanged: list[str]
|
||||
unknown: list[str]
|
||||
not_in_cache: list[str]
|
||||
|
||||
|
||||
def _diff(
|
||||
old: "DataIndex",
|
||||
new: "DataIndex",
|
||||
old: "BaseDataIndex",
|
||||
new: "BaseDataIndex",
|
||||
*,
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
granular: bool = False,
|
||||
not_in_cache: bool = False,
|
||||
not_in_remote: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
callback: "Callback" = DEFAULT_CALLBACK,
|
||||
) -> dict[str, list[str]]:
|
||||
from dvc_data.index import StorageError
|
||||
from dvc_data.index.diff import UNCHANGED, UNKNOWN, diff
|
||||
with_renames: bool = False,
|
||||
) -> DiffResult:
|
||||
from dvc_data.index.diff import (
|
||||
ADD,
|
||||
DELETE,
|
||||
MODIFY,
|
||||
RENAME,
|
||||
UNCHANGED,
|
||||
UNKNOWN,
|
||||
diff,
|
||||
)
|
||||
|
||||
ret: dict[str, list[str]] = {}
|
||||
ret: DiffResult = defaultdict(list) # type: ignore[assignment]
|
||||
change_types = {
|
||||
MODIFY: "modified",
|
||||
ADD: "added",
|
||||
DELETE: "deleted",
|
||||
RENAME: "renamed",
|
||||
}
|
||||
|
||||
def _add_change(typ, change):
|
||||
typ = _adapt_typ(typ)
|
||||
if typ not in ret:
|
||||
ret[typ] = []
|
||||
|
||||
ret[typ].append(_adapt_path(change))
|
||||
to_check: dict[FileSystem, dict[str, list[DataIndexEntry]]] = defaultdict(
|
||||
lambda: defaultdict(list)
|
||||
)
|
||||
|
||||
for change in diff(
|
||||
old,
|
||||
@@ -74,45 +141,72 @@ def _diff(
|
||||
shallow=not granular,
|
||||
hash_only=True,
|
||||
with_unknown=True,
|
||||
with_renames=with_renames,
|
||||
callback=callback,
|
||||
):
|
||||
typ = change.typ
|
||||
|
||||
# The index is a trie, so even when we filter by a specific path
|
||||
# like `dir/file`, all parent nodes leading to that path (e.g., `dir/`)
|
||||
# still appear in the view. As a result, keys like `dir/` will be present
|
||||
# even if only `dir/file` matches the filter.
|
||||
# We need to skip such entries to avoid showing root of tracked directories.
|
||||
if filter_keys:
|
||||
# RENAME does not have a `change.key`
|
||||
if typ == RENAME:
|
||||
assert change.new
|
||||
key = change.new.key
|
||||
# match with "new" key only
|
||||
assert key
|
||||
else:
|
||||
key = change.key
|
||||
|
||||
if not any(key[: len(fk)] == fk for fk in filter_keys):
|
||||
continue
|
||||
|
||||
if (
|
||||
change.typ == UNCHANGED
|
||||
typ == UNCHANGED
|
||||
and (not change.old or not change.old.hash_info)
|
||||
and (not change.new or not change.new.hash_info)
|
||||
):
|
||||
# NOTE: emulating previous behaviour
|
||||
continue
|
||||
|
||||
if change.typ == UNKNOWN and not change.new:
|
||||
if typ == UNKNOWN and not change.new:
|
||||
# NOTE: emulating previous behaviour
|
||||
continue
|
||||
|
||||
if (
|
||||
not_in_cache
|
||||
and change.old
|
||||
and change.old.hash_info
|
||||
and not old.storage_map.cache_exists(change.old)
|
||||
):
|
||||
# NOTE: emulating previous behaviour
|
||||
_add_change("not_in_cache", change)
|
||||
if not_in_cache and change.old and change.old.hash_info:
|
||||
old_entry = change.old
|
||||
cache_fs, cache_path = old.storage_map.get_cache(old_entry)
|
||||
# check later in batches
|
||||
to_check[cache_fs][cache_path].append(old_entry)
|
||||
|
||||
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
|
||||
change_typ = change_types.get(typ, typ)
|
||||
if typ == RENAME:
|
||||
assert change.old is not None
|
||||
assert change.new is not None
|
||||
ret["renamed"].append(
|
||||
{
|
||||
"old": _adapt_path_from_entry(change.old),
|
||||
"new": _adapt_path_from_entry(change.new),
|
||||
}
|
||||
)
|
||||
else:
|
||||
ret[change_typ].append(_adapt_path(change)) # type: ignore[literal-required]
|
||||
|
||||
_add_change(change.typ, change)
|
||||
|
||||
return ret
|
||||
total_items = sum(
|
||||
len(entries) for paths in to_check.values() for entries in paths.values()
|
||||
)
|
||||
with TqdmCallback(size=total_items, desc="Checking cache", unit="entry") as cb:
|
||||
missing_items = list(
|
||||
_get_missing_paths(
|
||||
to_check, batch_size=batch_size, callback=StorageCallback(cb)
|
||||
),
|
||||
)
|
||||
if missing_items:
|
||||
ret["not_in_cache"] = missing_items
|
||||
return dict(ret) # type: ignore[return-value]
|
||||
|
||||
|
||||
class GitInfo(TypedDict, total=False):
|
||||
@@ -151,7 +245,49 @@ 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]]:
|
||||
def filter_index(
|
||||
index: Union["DataIndex", "DataIndexView"],
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
) -> "BaseDataIndex":
|
||||
from dvc_data.index.view import DataIndexView
|
||||
|
||||
if not filter_keys:
|
||||
return index
|
||||
|
||||
if isinstance(index, DataIndexView):
|
||||
orig_index = index._index
|
||||
parent_filter_fn = index.filter_fn
|
||||
else:
|
||||
orig_index = index
|
||||
parent_filter_fn = None
|
||||
|
||||
def filter_fn(key: "DataIndexKey") -> bool:
|
||||
if parent_filter_fn is not None and not parent_filter_fn(key):
|
||||
return False
|
||||
|
||||
for filter_key in filter_keys:
|
||||
# eg: if key is "dir/file" and filter_key is "dir/", return True
|
||||
if key[: len(filter_key)] == filter_key:
|
||||
return True
|
||||
# eg: if key is `dir/` and filter_key is `dir/file`, also return True.
|
||||
# This ensures we include parent prefixes needed to reach matching leaves.
|
||||
# Intermediate prefixes must be retained to access nested keys.
|
||||
if filter_key[: len(key)] == key:
|
||||
return True
|
||||
return False
|
||||
|
||||
from dvc_data.index import view
|
||||
|
||||
return view(orig_index, filter_fn=filter_fn)
|
||||
|
||||
|
||||
def _diff_index_to_wtree(
|
||||
repo: "Repo",
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
granular: bool = False,
|
||||
batch_size: Optional[int] = None,
|
||||
with_renames: bool = False,
|
||||
) -> DiffResult:
|
||||
from .index import build_data_index
|
||||
|
||||
with ui.progress(desc="Building workspace index", unit="entry") as pb:
|
||||
@@ -162,37 +298,69 @@ def _diff_index_to_wtree(repo: "Repo", **kwargs: Any) -> dict[str, list[str]]:
|
||||
compute_hash=True,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
workspace_view = filter_index(workspace, filter_keys=filter_keys)
|
||||
|
||||
with ui.progress(
|
||||
desc="Calculating diff between index/workspace",
|
||||
unit="entry",
|
||||
) as pb:
|
||||
index = repo.index.data["repo"]
|
||||
view = filter_index(index, filter_keys=filter_keys)
|
||||
return _diff(
|
||||
repo.index.data["repo"],
|
||||
workspace,
|
||||
view,
|
||||
workspace_view,
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
not_in_cache=True,
|
||||
with_renames=with_renames,
|
||||
batch_size=batch_size,
|
||||
callback=pb.as_callback(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _diff_head_to_index(
|
||||
repo: "Repo", head: str = "HEAD", **kwargs: Any
|
||||
) -> dict[str, list[str]]:
|
||||
index = repo.index.data["repo"]
|
||||
repo: "Repo",
|
||||
head: str = "HEAD",
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
granular: bool = False,
|
||||
with_renames: bool = False,
|
||||
) -> DiffResult:
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.scm import RevError
|
||||
from dvc_data.index import DataIndex
|
||||
|
||||
with repo.switch(head):
|
||||
head_index = repo.index.data["repo"]
|
||||
index = repo.index.data["repo"]
|
||||
index_view = filter_index(index, filter_keys=filter_keys)
|
||||
|
||||
try:
|
||||
with repo.switch(head):
|
||||
head_index = repo.index.data["repo"]
|
||||
head_view = filter_index(head_index, filter_keys=filter_keys)
|
||||
except RevError:
|
||||
logger.debug("failed to switch to '%s'", head)
|
||||
head_view = DataIndex()
|
||||
except NotDvcRepoError as exc:
|
||||
# NOTE: this only gets raised on subdir repos at the moment,
|
||||
# which looks like a bug in `repo.switch`.
|
||||
logger.warning(exc)
|
||||
head_view = DataIndex()
|
||||
|
||||
with ui.progress(desc="Calculating diff between head/index", unit="entry") as pb:
|
||||
return _diff(head_index, index, callback=pb.as_callback(), **kwargs)
|
||||
return _diff(
|
||||
head_view,
|
||||
index_view,
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
with_renames=with_renames,
|
||||
callback=pb.as_callback(),
|
||||
)
|
||||
|
||||
|
||||
class Status(TypedDict):
|
||||
not_in_cache: list[str]
|
||||
not_in_remote: list[str]
|
||||
committed: dict[str, list[str]]
|
||||
uncommitted: dict[str, list[str]]
|
||||
committed: DiffResult
|
||||
uncommitted: DiffResult
|
||||
untracked: list[str]
|
||||
unchanged: list[str]
|
||||
git: GitInfo
|
||||
@@ -217,27 +385,158 @@ def _transform_git_paths_to_dvc(repo: "Repo", files: Iterable[str]) -> list[str]
|
||||
return [repo.fs.relpath(file, start) for file in files]
|
||||
|
||||
|
||||
def status(repo: "Repo", untracked_files: str = "no", **kwargs: Any) -> Status:
|
||||
def _get_entries_not_in_remote(
|
||||
repo: "Repo",
|
||||
filter_keys: Optional[Iterable["DataIndexKey"]] = None,
|
||||
granular: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
) -> list[str]:
|
||||
"""Get entries that are not in remote storage."""
|
||||
from dvc.repo.worktree import worktree_view
|
||||
from dvc_data.index import StorageKeyError
|
||||
|
||||
entries: dict[DataIndexKey, DataIndexEntry] = {}
|
||||
|
||||
def _onerror(entry, exc):
|
||||
if not isinstance(exc, DataIndexDirError):
|
||||
raise exc
|
||||
# We don't have the contents of this dir file, so we will only check this key.
|
||||
entries[entry.key] = entry
|
||||
|
||||
# View into the index, with only pushable entries
|
||||
index = worktree_view(repo.index, push=True)
|
||||
data_index = index.data["repo"]
|
||||
|
||||
orig_data_index_onerror = data_index.onerror
|
||||
data_index.onerror = _onerror
|
||||
|
||||
view = filter_index(data_index, filter_keys=filter_keys) # type: ignore[arg-type]
|
||||
|
||||
missing_entries = []
|
||||
|
||||
storage_map = view.storage_map
|
||||
|
||||
n = 0
|
||||
with TqdmCallback(size=n, desc="Checking remote", unit="entry") as cb:
|
||||
for key, entry in view.iteritems(shallow=not granular):
|
||||
if not (entry and entry.hash_info):
|
||||
continue
|
||||
|
||||
# The index is a trie, so even when we filter by a specific path
|
||||
# like `dir/file`, all parent nodes leading to that path (e.g., `dir/`)
|
||||
# still appear in the view. As a result, keys like `dir/` will be present
|
||||
# even if only `dir/file` matches the filter.
|
||||
# We need to skip such entries to avoid showing root of tracked directories.
|
||||
if filter_keys and not any(
|
||||
key[: len(filter_key)] == filter_key for filter_key in filter_keys
|
||||
):
|
||||
continue
|
||||
|
||||
entries[key] = entry
|
||||
n += 1
|
||||
cb.set_size(n)
|
||||
|
||||
for key, entry in entries.items():
|
||||
k = (*key, "") if entry.meta and entry.meta.isdir else key
|
||||
try:
|
||||
if not storage_map.remote_exists(entry, refresh=remote_refresh):
|
||||
missing_entries.append(os.path.sep.join(k))
|
||||
except StorageKeyError:
|
||||
pass
|
||||
finally:
|
||||
cb.relative_update()
|
||||
data_index.onerror = orig_data_index_onerror
|
||||
return missing_entries
|
||||
|
||||
|
||||
def _matches_target(p: str, targets: Iterable[str]) -> bool:
|
||||
sep = os.sep
|
||||
return any(p == t or p.startswith(t + sep) for t in targets)
|
||||
|
||||
|
||||
def _prune_keys(filter_keys: Iterable["DataIndexKey"]) -> list["DataIndexKey"]:
|
||||
sorted_keys = sorted(set(filter_keys), key=len)
|
||||
result: list[DataIndexKey] = []
|
||||
|
||||
for key in sorted_keys:
|
||||
if not any(key[: len(prefix)] == prefix for prefix in result):
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
|
||||
def status( # noqa: PLR0913
|
||||
repo: "Repo",
|
||||
targets: Optional[Iterable[Union[os.PathLike[str], str]]] = None,
|
||||
*,
|
||||
granular: bool = False,
|
||||
untracked_files: str = "no",
|
||||
remote: Optional[str] = None,
|
||||
not_in_remote: bool = False,
|
||||
remote_refresh: bool = False,
|
||||
config: Optional[dict] = None,
|
||||
batch_size: Optional[int] = None,
|
||||
head: str = "HEAD",
|
||||
with_renames: bool = True,
|
||||
) -> Status:
|
||||
from dvc.scm import NoSCMError, SCMError
|
||||
|
||||
head = kwargs.pop("head", "HEAD")
|
||||
uncommitted_diff = _diff_index_to_wtree(repo, **kwargs)
|
||||
config = config or {}
|
||||
if remote and not_in_remote:
|
||||
logger.debug("Using remote %r", remote)
|
||||
core = config.setdefault("core", {})
|
||||
core["remote"] = remote
|
||||
repo.config.merge(config)
|
||||
|
||||
targets = targets or []
|
||||
filter_keys: list[DataIndexKey] = [repo.fs.relparts(os.fspath(t)) for t in targets]
|
||||
# try to remove duplicate and overlapping keys
|
||||
filter_keys = _prune_keys(filter_keys)
|
||||
|
||||
uncommitted_diff = _diff_index_to_wtree(
|
||||
repo,
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
batch_size=batch_size,
|
||||
with_renames=with_renames,
|
||||
)
|
||||
unchanged = set(uncommitted_diff.pop("unchanged", []))
|
||||
|
||||
entries_not_in_remote: list[str] = []
|
||||
if not_in_remote:
|
||||
entries_not_in_remote = _get_entries_not_in_remote(
|
||||
repo,
|
||||
filter_keys=filter_keys,
|
||||
granular=granular,
|
||||
remote_refresh=remote_refresh,
|
||||
)
|
||||
|
||||
try:
|
||||
committed_diff = _diff_head_to_index(repo, head=head, **kwargs)
|
||||
committed_diff = _diff_head_to_index(
|
||||
repo,
|
||||
filter_keys=filter_keys,
|
||||
head=head,
|
||||
granular=granular,
|
||||
with_renames=with_renames,
|
||||
)
|
||||
except (SCMError, NoSCMError):
|
||||
committed_diff = {}
|
||||
else:
|
||||
unchanged &= set(committed_diff.pop("unchanged", []))
|
||||
|
||||
git_info = _git_info(repo.scm, untracked_files=untracked_files)
|
||||
untracked = git_info.get("untracked", [])
|
||||
untracked = _transform_git_paths_to_dvc(repo, untracked)
|
||||
scm_filter_targets = {
|
||||
os.path.relpath(os.path.abspath(t), repo.scm.root_dir) for t in targets
|
||||
}
|
||||
untracked_it: Iterable[str] = git_info.get("untracked", [])
|
||||
if scm_filter_targets:
|
||||
untracked_it = (
|
||||
f for f in untracked_it if _matches_target(f, scm_filter_targets)
|
||||
)
|
||||
untracked = _transform_git_paths_to_dvc(repo, untracked_it)
|
||||
# order matters here
|
||||
return Status(
|
||||
not_in_cache=uncommitted_diff.pop("not_in_cache", []),
|
||||
not_in_remote=uncommitted_diff.pop("not_in_remote", []),
|
||||
not_in_remote=entries_not_in_remote,
|
||||
committed=committed_diff,
|
||||
uncommitted=uncommitted_diff,
|
||||
untracked=untracked,
|
||||
|
||||
+25
-23
@@ -16,8 +16,7 @@ from dvc.types import StrPath
|
||||
from dvc_data.hashfile.meta import Meta
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dql.dataset import DatasetRecord # type: ignore[import]
|
||||
from dvcx.dataset import DatasetVersion # type: ignore[import]
|
||||
from datachain.dataset import DatasetRecord, DatasetVersion # type: ignore[import]
|
||||
from typing_extensions import Self
|
||||
|
||||
from dvc.repo import Repo
|
||||
@@ -30,10 +29,10 @@ def _get_dataset_record(name: str) -> "DatasetRecord":
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
try:
|
||||
from dvcx.catalog import get_catalog # type: ignore[import]
|
||||
from datachain.catalog import get_catalog # type: ignore[import]
|
||||
|
||||
except ImportError as exc:
|
||||
raise DvcException("dvcx is not installed") from exc
|
||||
raise DvcException("datachain is not installed") from exc
|
||||
|
||||
catalog = get_catalog()
|
||||
return catalog.get_remote_dataset(name)
|
||||
@@ -77,7 +76,7 @@ class SerDe:
|
||||
return asdict(self, filter=filter_defaults, value_serializer=value_serializer)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls: type["Self"], d: dict[str, Any]) -> "Self":
|
||||
def from_dict(cls, d: dict[str, Any]) -> "Self":
|
||||
_fields = fields(cast("type[AttrsInstance]", cls))
|
||||
kwargs = {f.name: d[f.name] for f in _fields if f.name in d}
|
||||
return cls(**kwargs)
|
||||
@@ -87,7 +86,7 @@ class SerDe:
|
||||
class DatasetSpec(SerDe):
|
||||
name: str
|
||||
url: str
|
||||
type: Literal["dvc", "dvcx", "url"]
|
||||
type: Literal["dvc", "dc", "url"]
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
@@ -109,7 +108,7 @@ class DVCDatasetLock(DVCDatasetSpec):
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCXDatasetLock(DatasetSpec):
|
||||
class DatachainDatasetLock(DatasetSpec):
|
||||
version: int
|
||||
created_at: datetime = field(converter=to_datetime)
|
||||
|
||||
@@ -161,13 +160,13 @@ class DVCDataset:
|
||||
|
||||
|
||||
@frozen(kw_only=True)
|
||||
class DVCXDataset:
|
||||
class DatachainDataset:
|
||||
manifest_path: str
|
||||
spec: "DatasetSpec"
|
||||
lock: "Optional[DVCXDatasetLock]" = field(default=None)
|
||||
lock: "Optional[DatachainDatasetLock]" = field(default=None)
|
||||
_invalidated: bool = field(default=False, eq=False, repr=False)
|
||||
|
||||
type: ClassVar[Literal["dvcx"]] = "dvcx"
|
||||
type: ClassVar[Literal["dc"]] = "dc"
|
||||
|
||||
@property
|
||||
def pinned(self) -> bool:
|
||||
@@ -194,7 +193,7 @@ class DVCXDataset:
|
||||
name, _version = self.name_version
|
||||
version = version if version is not None else _version
|
||||
version_info = _get_dataset_info(name, record=record, version=version)
|
||||
lock = DVCXDatasetLock(
|
||||
lock = DatachainDatasetLock(
|
||||
**self.spec.to_dict(),
|
||||
version=version_info.version,
|
||||
created_at=version_info.created_at,
|
||||
@@ -227,9 +226,9 @@ class URLDataset:
|
||||
return evolve(self, lock=lock)
|
||||
|
||||
|
||||
Lock = Union[DVCDatasetLock, DVCXDatasetLock, URLDatasetLock]
|
||||
Lock = Union[DVCDatasetLock, DatachainDatasetLock, URLDatasetLock]
|
||||
Spec = Union[DatasetSpec, DVCDatasetSpec]
|
||||
Dataset = Union[DVCDataset, DVCXDataset, URLDataset]
|
||||
Dataset = Union[DVCDataset, DatachainDataset, URLDataset]
|
||||
|
||||
|
||||
class DatasetNotFoundError(DvcException, KeyError):
|
||||
@@ -243,7 +242,7 @@ class DatasetNotFoundError(DvcException, KeyError):
|
||||
|
||||
class Datasets(Mapping[str, Dataset]):
|
||||
def __init__(self, repo: "Repo") -> None:
|
||||
self.repo: "Repo" = repo
|
||||
self.repo: Repo = repo
|
||||
|
||||
def __repr__(self):
|
||||
return repr(dict(self))
|
||||
@@ -308,13 +307,13 @@ class Datasets(Mapping[str, Dataset]):
|
||||
raise ValueError("type should be present in spec")
|
||||
if typ == "dvc":
|
||||
return DVCDatasetSpec.from_dict(spec)
|
||||
if typ in {"dvcx", "url"}:
|
||||
if typ in {"dc", "url"}:
|
||||
return DatasetSpec.from_dict(spec)
|
||||
raise ValueError(f"unknown dataset type: {spec.get('type', '')}")
|
||||
|
||||
@staticmethod
|
||||
def _lock_from_info(lock: Optional[dict[str, Any]]) -> Optional[Lock]:
|
||||
kl = {"dvc": DVCDatasetLock, "dvcx": DVCXDatasetLock, "url": URLDatasetLock}
|
||||
kl = {"dvc": DVCDatasetLock, "dc": DatachainDatasetLock, "url": URLDatasetLock}
|
||||
if lock and (cls := kl.get(lock.get("type", ""))): # type: ignore[assignment]
|
||||
return cls.from_dict(lock) # type: ignore[attr-defined]
|
||||
return None
|
||||
@@ -357,9 +356,9 @@ class Datasets(Mapping[str, Dataset]):
|
||||
lock=lock,
|
||||
invalidated=_invalidated,
|
||||
)
|
||||
if spec.type == "dvcx":
|
||||
assert lock is None or isinstance(lock, DVCXDatasetLock)
|
||||
return DVCXDataset(
|
||||
if spec.type == "dc":
|
||||
assert lock is None or isinstance(lock, DatachainDatasetLock)
|
||||
return DatachainDataset(
|
||||
manifest_path=manifest_path,
|
||||
spec=spec,
|
||||
lock=lock,
|
||||
@@ -375,7 +374,7 @@ class Datasets(Mapping[str, Dataset]):
|
||||
manifest_path: StrPath = "dvc.yaml",
|
||||
**kwargs: Any,
|
||||
) -> Dataset:
|
||||
assert type in {"dvc", "dvcx", "url"}
|
||||
assert type in {"dvc", "dc", "url"}
|
||||
kwargs.update({"name": name, "url": url, "type": type})
|
||||
dataset = self._build_dataset(os.path.abspath(manifest_path), kwargs)
|
||||
dataset = dataset.update(self.repo)
|
||||
@@ -390,13 +389,16 @@ class Datasets(Mapping[str, Dataset]):
|
||||
|
||||
if dataset.type == "url" and (version or kwargs.get("rev")):
|
||||
raise ValueError("cannot update version/revision for a url")
|
||||
if dataset.type == "dvcx" and version is not None:
|
||||
if dataset.type == "dc" and version is not None:
|
||||
if not isinstance(version, int):
|
||||
raise TypeError(
|
||||
f"dvcx version has to be an integer, got {type(version).__name__!r}"
|
||||
"DataChain dataset version has to be an integer, "
|
||||
f"got {type(version).__name__!r}"
|
||||
)
|
||||
if version < 1:
|
||||
raise ValueError(f"dvcx version should be >=1, got {version}")
|
||||
raise ValueError(
|
||||
f"DataChain dataset version should be >=1, got {version}"
|
||||
)
|
||||
|
||||
new = dataset.update(self.repo, **kwargs)
|
||||
|
||||
|
||||
+7
-1
@@ -26,7 +26,7 @@ def _diff(old, new, data_keys, 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]]" = {
|
||||
ret: dict[str, list[dict]] = {
|
||||
"added": [],
|
||||
"deleted": [],
|
||||
"modified": [],
|
||||
@@ -45,6 +45,12 @@ def _diff(old, new, data_keys, with_missing=False):
|
||||
with_renames=True,
|
||||
meta_cmp_key=meta_cmp_key,
|
||||
roots=data_keys,
|
||||
# Include unknown entries from missing dir entry, so that they don't
|
||||
# get reported as added/modified/deleted.
|
||||
# Also return unchanged entries so that we can check if they are missing
|
||||
# from cache.
|
||||
with_unknown=True,
|
||||
with_unchanged=with_missing,
|
||||
):
|
||||
if (change.old and change.old.isdir and not change.old.hash_info) or (
|
||||
change.new and change.new.isdir and not change.new.hash_info
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ def du(
|
||||
path: Optional[str] = None,
|
||||
rev: Optional[str] = None,
|
||||
summarize: bool = False,
|
||||
config: Union[None, dict[str, Any], str] = None,
|
||||
config: Union[dict[str, Any], str, None] = None,
|
||||
remote: Optional[str] = None,
|
||||
remote_config: Optional[dict] = None,
|
||||
):
|
||||
|
||||
@@ -119,7 +119,7 @@ class Experiments:
|
||||
**kwargs,
|
||||
):
|
||||
"""Reproduce and checkout a single (standalone) experiment."""
|
||||
exp_queue: "BaseStashQueue" = (
|
||||
exp_queue: BaseStashQueue = (
|
||||
self.tempdir_queue if tmp_dir else self.workspace_queue
|
||||
)
|
||||
self.queue_one(exp_queue, **kwargs)
|
||||
@@ -205,7 +205,7 @@ class Experiments:
|
||||
Experiment will be derived from the current workspace.
|
||||
"""
|
||||
|
||||
name = kwargs.get("name", None)
|
||||
name = kwargs.get("name")
|
||||
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
|
||||
|
||||
if name:
|
||||
@@ -249,7 +249,7 @@ class Experiments:
|
||||
exec_results = queue.reproduce(copy_paths=copy_paths, message=message)
|
||||
|
||||
results: dict[str, str] = {}
|
||||
for _, exp_result in exec_results.items():
|
||||
for exp_result in exec_results.values():
|
||||
results.update(exp_result)
|
||||
return results
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
|
||||
from dvc.repo.checkout import checkout as dvc_checkout
|
||||
from dvc.scm import RevError, resolve_rev
|
||||
|
||||
exps: "Experiments" = repo.experiments
|
||||
exps: Experiments = repo.experiments
|
||||
|
||||
is_stash: bool = False
|
||||
|
||||
@@ -57,7 +57,7 @@ def apply(repo: "Repo", rev: str, **kwargs):
|
||||
|
||||
|
||||
def _apply(repo: "Repo", rev: str, name: Optional[str] = None, is_stash: bool = False):
|
||||
exps: "Experiments" = repo.experiments
|
||||
exps: Experiments = repo.experiments
|
||||
|
||||
with exps.apply_stash.preserve_workspace(rev, name=name):
|
||||
with repo.scm.detach_head(rev, force=True):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user