Compare commits

...
84 Commits
Author SHA1 Message Date
skshetry eec3d48731 pyproject.toml: remove stale comment from dependency (#10546) 2024-08-30 18:51:56 +05:45
skshetry 5417ccd7b6 dvcfs.get: simplify and separate files into respective filesystems while walking (#10544)
dvcfs.get: simplify and separate files into respective filesystems while collecting/walking
2024-08-29 16:47:16 +00:00
skshetry f2b7b00af5 dvcfilesystem.get: prefer downloading from repo.fs before dvc.fs (#10543) 2024-08-29 13:06:21 +00:00
skshetry 4f3fb15f2e dvcfs: optimize get() by reducing index.info calls() (#10540) 2024-08-27 18:08:40 +05:45
pre-commit-ci[bot] 016f285fe3 [pre-commit.ci] pre-commit autoupdate (#10541)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-27 06:43:45 +05:45
dependabot[bot] f6dd641855 build(deps): bump mypy from 1.11.1 to 1.11.2 (#10539)
Bumps [mypy](https://github.com/python/mypy) from 1.11.1 to 1.11.2.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.1...v1.11.2)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-26 08:21:55 +05:45
Saugat Pachhai (सौगात) 3f2584d155 bench: add --benchmark-cprofile-dump 2024-08-22 14:35:23 +05:45
skshetry 705b7bbea6 simplify running benchmarks (#10536) 2024-08-21 21:09:59 +05:45
Saugat Pachhai (सौगात) 26ad01bec0 bench: drop add/checkout from test_sharing 2024-08-21 08:00:19 +05:45
Saugat Pachhai (सौगात) e76aaeac57 benchmarks: fix for local versions 2024-08-20 21:32:37 +05:45
skshetry 6f24d04b5e ci (bench): skip benchmark for test_import on versions >=3.53,<3.54.1 (#10535) 2024-08-20 21:22:08 +05:45
skshetry 2dcaea8119 dependency/repo: do not suppress OSError on info() (#10533) 2024-08-20 18:14:21 +05:45
Dave BerenbaumandSaugat Pachhai 81db9b4572 save import hash info to state (#10531)
* save import hash info to state

* clean up path handling

* avoid walking

* tests: do not use as_posix to check for output

---------

Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-08-20 13:41:37 +05:45
pre-commit-ci[bot] 98dde6cd27 [pre-commit.ci] pre-commit autoupdate (#10532)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.0 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.0...v0.6.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-20 04:19:17 +05:45
skshetry e42d094310 remove epic template (#10530) 2024-08-17 09:40:57 +05:45
skshetry 38ee0a01f7 update ruff to 0.6 (#10526) 2024-08-15 20:02:09 +05:45
dependabot[bot]andSaugat Pachhai d494126cc5 build(deps): bump mypy from 1.10.1 to 1.11.1 (#10495)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-08-15 19:59:05 +05:45
skshetry fd3b43df05 fix test_daemon.py::test_analytics flakiness (#10523)
Turns out `iterative-telemetry` spends a long time trying to
generate an id. Sometimes it gets frozen for a long time (>30s).
2024-08-14 16:55:18 +05:45
Saugat Pachhai (सौगात) b9bcb45aaa ci: add python version for cache suffix 2024-08-14 15:17:31 +05:45
skshetry 7bca71db2e ci: use pytest-split based on least_duration (#10522) 2024-08-14 14:58:55 +05:45
skshetry 52405a0e10 ci: use uv to install dependencies (#10507)
* ci: use uv to install dependencies

* split Windows tests into four for each python versions

* split tests and check into two jobs

* increase timeout
2024-08-14 13:24:56 +05:45
skshetry c514b0ea31 use uv for benchmark tests while installing/creating virtualenv (#10520) 2024-08-13 22:27:05 +05:45
skshetry 853116a0e3 bump dvc-data to >=3.16.2 (#10519) 2024-08-13 08:59:06 +05:45
skshetry 5e4daf6cfb deps: bump dvc-data to >=3.16.0 (#10513) 2024-08-13 07:15:05 +05:45
pre-commit-ci[bot] 991a816995 [pre-commit.ci] pre-commit autoupdate (#10518)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-13 06:40:30 +05:45
Jan Stratil 9735b5aaa9 dvc dependency repo: Catch FileNotFoundError (#10501)
- When checkouting non-existent file the _checkout_file recursively
  calls os.stat on a symlinked file which destination doesn't exist.
  This causes a FileNotFoundError but it is not catched and it causes
  an unexpected error instead.

Fixes #10500.
2024-08-12 20:49:26 +05:45
Ivan Shcheklein fa317d5b1d fix(readme): add style to datachain banner (#10517) 2024-08-12 13:36:48 +05:45
Ivan Shcheklein 1b0fb6487b add datachain link (#10514) 2024-08-08 10:57:31 -07:00
Saugat Pachhai (सौगात) 43e6229308 stabilize import db: remove experimental warning 2024-08-07 08:40:56 -04:00
skshetry 1bef0da4fb import_db: set dependency.is_in_repo to false (#10504) 2024-08-06 14:02:24 +05:45
pre-commit-ci[bot] 284d83f390 [pre-commit.ci] pre-commit autoupdate (#10493)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-06 06:51:31 +05:45
Dave Berenbaum 417e6d4b76 revert to --no-run-cache default for push/pull/fetch 2024-07-30 14:23:24 -04:00
dberenbaum 248ba3b6b6 limit reimport behavior to checkout 2024-07-24 09:37:32 -04:00
dberenbaum 8a91c2b303 add back code to avoid downloading imports 2024-07-24 09:37:32 -04:00
Dave Berenbaum 544dad75e9 try force overwriting run cache entries on push 2024-07-24 08:15:01 -04:00
pre-commit-ci[bot] 8f50f2509f [pre-commit.ci] pre-commit autoupdate (#10487)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-23 07:58:55 +05:45
pre-commit-ci[bot] cdc1857559 [pre-commit.ci] pre-commit autoupdate (#10484)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.1 → v0.5.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.1...v0.5.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-15 13:42:20 -07:00
Dave Berenbaum 358dd163da set lower bound on fsspec (#10479) 2024-07-11 17:53:37 +05:45
pre-commit-ci[bot] aae1a93cb5 [pre-commit.ci] pre-commit autoupdate (#10478)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.5.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.5.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-08 14:22:12 -07:00
pre-commit-ci[bot]andIvan Shcheklein c9b7218659 [pre-commit.ci] pre-commit autoupdate (#10473)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.0)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* lint daemon.py

* lint run.py

* lint fixtures.py

* lint checkout.py

* lint collect.py

* lint index.py

* lint index.py

* lint checkout.py

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ivan Shcheklein <[email protected]>
2024-07-06 17:05:42 +00:00
dependabot[bot] 88c4b9525d build(deps): update flufl-lock requirement from <8,>=5 to >=8.1.0,<9 (#10470)
---
updated-dependencies:
- dependency-name: flufl-lock
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:25:45 -07:00
pre-commit-ci[bot] 69d36eeb06 [pre-commit.ci] pre-commit autoupdate (#10466)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-27 20:09:40 +05:45
3201794553 chore(studio): Update Studio URL to new domain (#10468)
Co-authored-by: Marcin Jasion <[email protected]>
Co-authored-by: skshetry <[email protected]>
2024-06-27 16:43:17 +05:45
dependabot[bot] 4464f8b4b9 build(deps): bump mypy from 1.10.0 to 1.10.1 (#10467)
Bumps [mypy](https://github.com/python/mypy) from 1.10.0 to 1.10.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.10.0...v1.10.1)

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

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-24 21:00:48 -07:00
pre-commit-ci[bot] efc7e05893 [pre-commit.ci] pre-commit autoupdate (#10463)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-18 08:21:54 +05:45
skshetry a555c3d29a skip stage cache push/pull test for http (#10462) 2024-06-15 21:51:20 +05:45
George Geddes 676645b1ca Read studio hostname from config when not present on cli or env (#10459) 2024-06-14 17:29:56 -07:00
pre-commit-ci[bot] 93c3100bad [pre-commit.ci] pre-commit autoupdate (#10455)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.4.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-12 06:41:09 +05:45
pre-commit-ci[bot] c74156288d [pre-commit.ci] pre-commit autoupdate (#10443)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-03 17:40:31 +05:45
George KussumotoandSaugat Pachhai 0e38ccc031 fix(pull): normalize dependency path (#10446)
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-06-03 16:43:43 +05:45
Ivan Shcheklein b69b368ffe fix(benchmarks): force older pygit2 for older DVC versions (#10447) 2024-06-03 15:56:43 +05:45
6cc80fdeaa fix fish shell support (#10445)
Co-authored-by: Alexey Stern [email protected]
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
2024-06-03 15:45:21 +05:45
Ivan Shcheklein 8f8d49d023 fix(push/pull): don't fail on unsupported run cache (#10441) 2024-05-27 14:37:44 +05:45
Dave Berenbaum 83ec952da1 improve repro --pull --run-cache test 2024-05-23 14:26:42 -04:00
Dave Berenbaum c0d7e2a8de test repro --pull failure 2024-05-23 14:26:42 -04:00
Dave Berenbaum 9aad09164c update repro --pull description 2024-05-23 14:26:42 -04:00
Dave Berenbaum 1f7733fec1 refactor 2024-05-23 14:26:42 -04:00
Dave Berenbaum 856c902b65 add tests 2024-05-23 14:26:42 -04:00
Dave Berenbaum ebb459f9e2 refactor dvc repro --pull 2024-05-23 14:26:42 -04:00
Dave Berenbaum 2e0b024efe Apply suggestions from code review 2024-05-23 13:45:48 -04:00
Dave Berenbaum 135fa26515 Update dvc/repo/artifacts.py 2024-05-23 13:45:48 -04:00
Dave Berenbaum ef507efa04 artifacts: get studio info from local config 2024-05-23 13:45:48 -04:00
Dave Berenbaum 05db79e7cf add dvc artifacts get --show-url 2024-05-23 12:46:21 -04:00
Dave Berenbaum 0a813bbcbc push/pull run cache by default 2024-05-22 08:21:55 -04:00
skshetry e82356ba92 deps: bump scmrepo to >=3.3.4 (#10432)
Closes https://github.com/iterative/dvc/issues/10431.

Upstream PR: https://github.com/iterative/scmrepo/pull/359
2024-05-20 18:13:25 +05:45
Sigurd Spieckermann 5bfacec974 import/get: escape non-pattern LFS path parts (#10416) 2024-05-13 10:24:06 -07:00
pre-commit-ci[bot] 1848074ac9 [pre-commit.ci] pre-commit autoupdate (#10409)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.1 → v0.4.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.1...v0.4.3)

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-10 10:52:02 +00:00
Sigurd Spieckermann a7c0f6ef39 import/get: optimize LFS prefetching (#10414) 2024-05-03 08:58:53 -07:00
Thomas Roehr 0a8a9deceb Update s3 dependency to dvc-s3>=3.2.0 2024-04-26 11:01:03 -04:00
Thomas Roehr c5d98681c0 s3: align config name for anonymous login 2024-04-26 11:01:03 -04:00
Thomas Roehr 15e05e4b3e dvc: s3: allow configuring anonymous access 2024-04-26 11:01:03 -04:00
pre-commit-ci[bot] 7df8b0c09b [pre-commit.ci] pre-commit autoupdate (#10397)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-26 15:25:52 +05:45
Amirmohammad Sadat Shokouhi 2e1faf2979 add replication factor argument for hdfs in config schema (#10396) 2024-04-25 12:03:35 -07:00
skshetry cbaeff2ea1 deps: bump scmrepo minver to >=3.3.2,<4 (#10406)
Update pyproject.toml
2024-04-25 14:41:58 +05:45
dependabot[bot] 132e625ae8 build(deps): bump mypy from 1.9.0 to 1.10.0 (#10405)
Bumps [mypy](https://github.com/python/mypy) from 1.9.0 to 1.10.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 09:54:50 +05:45
skshetry 8517cccff4 artifacts: add support for monorepo (#10386) 2024-04-16 21:13:02 +05:45
dberenbaum c52a25e011 fix tests 2024-04-16 11:15:52 -04:00
dberenbaum c4678736b0 add option to not relink 2024-04-16 11:15:52 -04:00
pre-commit-ci[bot] ec92cc14d1 [pre-commit.ci] pre-commit autoupdate (#10390)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-16 10:37:51 +05:45
Dave Berenbaum e6048287cb fix test for dvc-render updates to plots title anchor (#10387) 2024-04-11 19:23:03 +05:45
pre-commit-ci[bot] 72a353d8b9 [pre-commit.ci] pre-commit autoupdate (#10384)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-09 06:45:19 +05:45
pre-commit-ci[bot] dd2b515c97 [pre-commit.ci] pre-commit autoupdate (#10376)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-02 06:52:09 +05:45
skshetry c223cdaecd params/metrics: do not fail during dir expansion (#10375)
params/metrics: assume file when dir expansion raises error

Closes https://github.com/iterative/dvc/issues/10018.

We have to be careful when expanding paths. This PR fall backs to assume it
as a file when either of `isdir` and `find` raises error due to some mishaps.
The consequence of this assumption happens when we try to read the file.
In that case, if the directory exists, and we happen to try to open it
as a file, dvc will raise `IsADirectoryError`, but that is handled correctly
and won't fail catastrophically like this did.
2024-03-28 07:30:12 +05:45
skshetry 5379aa2348 add: raise DoesNotExistError only if the output path does not exist (#10373)
`dvc add` command incorrectly raises a `DoesNotExistError` when a
broken symlink exists in an output directory, and the target name
is same as the directory's name.

eg: If `data` is an output, and is the command is invoked as `dvc add data`
(i.e. no virtual directory operations to perform).

The expected behavior to raise a `FileNotFoundError`.
`DoesNotExistError` should only be raised if the output itself does not
exist.

Related: https://github.com/iterative/dvc/issues/3717
2024-03-26 11:16:35 +00:00
114 changed files with 4948 additions and 479 deletions
File diff suppressed because it is too large Load Diff
-60
View File
@@ -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
+25 -3
View File
@@ -5,6 +5,10 @@ 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
@@ -12,7 +16,25 @@ jobs:
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- uses: iterative/dvc-bench@main
python-version: "3.12"
- uses: hynek/setup-cached-uv@v2
- uses: actions/checkout@v4
with:
pytest_options: "-k 'test_init or test_help'"
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- 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@v4
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'
+3 -2
View File
@@ -27,14 +27,15 @@ jobs:
with:
python-version: 3.12
- run: python -m pip install --upgrade pip setuptools_scm build twine
- uses: hynek/setup-cached-uv@v2
- run: uv pip install --upgrade setuptools_scm build twine --system
- 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
- run: echo 'PKG = "pip"'>dvc/_build.py
- run: python -m build
- run: python -m build --installer=uv
env:
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_DVC: ${{ env.version }}
- run: twine check --strict dist/*
+8 -7
View File
@@ -18,8 +18,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
pyv: ["3.10"]
os: [ubuntu-latest]
pyv: ["3.12"]
plugin: ["dvc-s3"]
steps:
@@ -37,16 +37,17 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.pyv }}
cache: pip
- uses: hynek/setup-cached-uv@v2
with:
cache-dependency-path: |
dvc/pyproject.toml
${{ matrix.plugin }}/setup.cfg
${{ matrix.plugin }}/pyproject.toml
- 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
+66 -12
View File
@@ -16,15 +16,68 @@ permissions:
contents: read
jobs:
tests:
timeout-minutes: 50
lint:
timeout-minutes: 10
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, windows-latest]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: hynek/setup-cached-uv@v2
with:
cache-dependency-path: 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: 20
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
pyv: ["3.9", "3.10", "3.11", "3.12"]
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.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"
steps:
- uses: actions/checkout@v4
with:
@@ -33,20 +86,21 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.pyv }}
cache: pip
- uses: hynek/setup-cached-uv@v2
with:
cache-suffix: ${{ matrix.pyv }}
cache-dependency-path: pyproject.toml
- name: install
run: |
pip install --upgrade pip wheel
pip install -e ".[dev]"
- uses: pre-commit/[email protected]
- 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:
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:
+3 -3
View File
@@ -6,7 +6,7 @@ ci:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.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.3.4'
rev: 'v0.6.2'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies: ["tomli"]
+3 -1
View File
@@ -1,4 +1,6 @@
|Banner|
🚀 Check out our new product `DataChain <https://github.com/iterative/datachain>`_ (and give it a ⭐!) if you need to version and process a large number of files. Contact us at [email protected] to discuss commercial solutions and support for AI reproducibility and data management scenarios.
--------------------------
`Website <https://dvc.org>`_
• `Docs <https://dvc.org/doc>`_
+13 -2
View File
@@ -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)}
+1 -1
View File
@@ -285,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
+2 -2
View File
@@ -18,8 +18,8 @@ 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)
self.config: Config = self.repo.config
self.args = args
def do_run(self):
+8
View File
@@ -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("")
@@ -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
+33
View 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",
+8
View File
@@ -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 -1
View File
@@ -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)
+3 -3
View File
@@ -194,7 +194,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.",
)
@@ -262,7 +262,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.",
)
@@ -324,7 +324,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.",
)
+1 -1
View File
@@ -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 "", ")")
+3 -1
View File
@@ -1,3 +1,5 @@
import argparse
from dvc.cli import formatter
from dvc.cli.command import CmdBase
from dvc.cli.utils import append_doc_link
@@ -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.",
)
+2 -1
View File
@@ -1,3 +1,4 @@
import argparse
from typing import Any
from dvc.cli import completion, formatter
@@ -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.",
)
+1 -1
View File
@@ -60,7 +60,7 @@ class CmdGC(CmdBase):
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."
+1 -1
View File
@@ -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
+1 -4
View File
@@ -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",
+1 -1
View File
@@ -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
+7 -2
View File
@@ -18,11 +18,16 @@ class CmdStudioLogin(CmdConfig):
from dvc.utils.studio import STUDIO_URL
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_URL
hostname = (
self.args.hostname
or os.environ.get(DVC_STUDIO_URL)
or studio.get("url")
or STUDIO_URL
)
scopes = self.args.scopes
studio = self.config.get("studio", {})
if studio.get("url", hostname) == hostname and "token" in studio:
raise DvcException(
"Token already exists. "
+1 -1
View File
@@ -160,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)
+7 -1
View File
@@ -193,6 +193,7 @@ SCHEMA = {
"session_token": str,
Optional("listobjects", default=False): Bool, # obsoleted
Optional("use_ssl", default=True): Bool,
Optional("allow_anonymous_login", default=False): Bool,
"ssl_verify": Any(Bool, str),
"sse": str,
"sse_kms_key_id": str,
@@ -232,7 +233,12 @@ SCHEMA = {
Optional("verify", default=False): Bool,
**REMOTE_COMMON,
},
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
"hdfs": {
"user": str,
"kerb_ticket": str,
"replication": int,
**REMOTE_COMMON,
},
"webhdfs": {
"kerberos": Bool,
"kerberos_principal": str,
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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()
+7 -3
View File
@@ -37,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"
@@ -70,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):
+24 -1
View File
@@ -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
+17 -5
View File
@@ -1,3 +1,5 @@
import glob
from itertools import repeat
from typing import Optional
from urllib.parse import urlparse
@@ -46,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):
@@ -60,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
]
@@ -68,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):
+170 -7
View File
@@ -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"]]
@@ -360,7 +366,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 +435,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 +480,127 @@ 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
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.mkdir(d)
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}
return fs.get_file(src, dest, callback=callback, **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(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 +609,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 +677,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
+1 -1
View File
@@ -123,7 +123,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)}"
)
+3 -2
View File
@@ -4,6 +4,7 @@ import hashlib
import os
from abc import ABC, abstractmethod
from datetime import timedelta
from typing import Optional, Union
import flufl.lock
import zc.lockfile
@@ -174,9 +175,9 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
self._owned = True
self._retry_errnos = []
def lock(self):
def lock(self, timeout: Optional[Union[timedelta, int]] = None):
try:
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
super().lock(timeout or timedelta(seconds=DEFAULT_TIMEOUT))
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE) # noqa: B904
+14 -12
View File
@@ -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
@@ -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,8 @@ class Output:
granular = (
self.is_dir_checksum and filter_info and filter_info != self.fs_path
)
# NOTE: trying to use hardlink during transfer only if we will be
# relinking later
hardlink = relink
if granular:
obj = self._commit_granular_dir(filter_info, hardlink)
obj = self._commit_granular_dir(filter_info, hardlink=False)
else:
staging, _, obj = self._build(
self.cache,
@@ -771,7 +771,7 @@ class Output:
self.cache,
{obj.hash_info},
shallow=False,
hardlink=hardlink,
hardlink=False,
callback=cb,
)
if relink:
@@ -786,6 +786,7 @@ class Output:
state=self.repo.state,
prompt=prompt.confirm,
progress_callback=cb,
old=obj,
)
self.set_exec()
@@ -902,7 +903,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 +1159,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:
@@ -1353,7 +1354,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 +1366,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
@@ -1403,7 +1404,7 @@ class Output:
staging,
self.cache,
{obj.hash_info},
hardlink=relink,
hardlink=False,
shallow=False,
callback=cb,
)
@@ -1421,6 +1422,7 @@ class Output:
state=self.repo.state,
prompt=prompt.confirm,
progress_callback=callback,
old=obj,
)
self.set_exec()
return obj
+2 -2
View File
@@ -563,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
+1 -1
View File
@@ -41,7 +41,7 @@ class Tqdm(tqdm):
"miniters": 1,
}
def __init__( # noqa: PLR0913
def __init__(
self,
iterable=None,
disable=None,
+1 -1
View File
@@ -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):
+11 -6
View File
@@ -147,7 +147,7 @@ class Repo:
):
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,7 +161,7 @@ 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
@@ -184,12 +184,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()
@@ -206,6 +206,11 @@ class Repo:
friendly=True,
)
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()
+14 -3
View File
@@ -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
+65 -22
View File
@@ -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
@@ -230,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
@@ -248,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,
@@ -277,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(
@@ -287,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,
@@ -304,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,
@@ -314,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
@@ -332,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(
+1 -1
View File
@@ -125,7 +125,7 @@ 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
+2 -2
View File
@@ -76,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)
@@ -242,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))
+1 -1
View File
@@ -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": [],
+2 -2
View File
@@ -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)
@@ -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
+2 -2
View File
@@ -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):
+5 -5
View File
@@ -69,11 +69,11 @@ def collect_rev(
def _collect_rev(
repo: "Repo",
rev: str,
revision: str,
param_deps: bool = False,
**kwargs,
) -> SerializableExp:
with repo.switch(rev) as rev:
with repo.switch(revision) as rev:
if rev == "workspace":
timestamp: Optional[datetime] = None
else:
@@ -174,7 +174,7 @@ def collect_active(
"""
if not baseline_revs:
return {}
result: dict[str, list["ExpRange"]] = {}
result: dict[str, list[ExpRange]] = {}
exps = repo.experiments
for queue in (exps.workspace_queue, exps.tempdir_queue, exps.celery_queue):
for baseline, active_exps in queue.collect_active_data(
@@ -220,7 +220,7 @@ def collect_successful(
Returns:
Dict mapping baseline revision to successful experiments.
"""
result: dict[str, list["ExpRange"]] = {}
result: dict[str, list[ExpRange]] = {}
for baseline_rev in baseline_revs:
result[baseline_rev] = list(_collect_baseline(repo, baseline_rev, **kwargs))
return result
@@ -304,7 +304,7 @@ def collect(
)
workspace_data = collect_rev(repo, "workspace", **kwargs)
result: list["ExpState"] = [workspace_data]
result: list[ExpState] = [workspace_data]
queued = collect_queued(repo, baseline_revs, **kwargs) if not hide_queued else {}
active = collect_active(repo, baseline_revs, **kwargs)
failed = collect_failed(repo, baseline_revs, **kwargs) if not hide_failed else {}
+4 -4
View File
@@ -199,7 +199,7 @@ class BaseExecutor(ABC):
@classmethod
def from_info(cls, info: "ExecutorInfo") -> "Self":
if info.result_hash:
result: Optional["ExecutorResult"] = ExecutorResult(
result: Optional[ExecutorResult] = ExecutorResult(
info.result_hash,
(ExpRefInfo.from_ref(info.result_ref) if info.result_ref else None),
info.result_force,
@@ -288,7 +288,7 @@ class BaseExecutor(ABC):
stages = []
if targets:
for target in targets:
stages.append(
stages.append( # noqa: PERF401
dvc.commit(
target, recursive=recursive, force=True, relink=False
)
@@ -470,7 +470,7 @@ class BaseExecutor(ABC):
cls._set_log_level(log_level)
exp_hash: Optional[str] = None
exp_ref: Optional["ExpRefInfo"] = None
exp_ref: Optional[ExpRefInfo] = None
repro_force: bool = False
if info.name:
@@ -559,7 +559,7 @@ class BaseExecutor(ABC):
)
ref: Optional[str] = dvc.scm.get_ref(EXEC_BRANCH, follow=False)
exp_ref: Optional["ExpRefInfo"] = ExpRefInfo.from_ref(ref) if ref else None
exp_ref: Optional[ExpRefInfo] = ExpRefInfo.from_ref(ref) if ref else None
if cls.WARN_UNTRACKED:
untracked = dvc.scm.untracked_files()
if untracked:
+1 -1
View File
@@ -201,7 +201,7 @@ class WorkspaceExecutor(BaseLocalExecutor):
@classmethod
def from_stash_entry(cls, repo: "Repo", entry: "ExpStashEntry", **kwargs):
root_dir = repo.scm.root_dir
executor: "WorkspaceExecutor" = cls._from_stash_entry(
executor: WorkspaceExecutor = cls._from_stash_entry(
repo, entry, root_dir, **kwargs
)
logger.debug("Init workspace executor in '%s'", root_dir)
+3 -3
View File
@@ -30,7 +30,7 @@ def pull( # noqa: C901
pull_cache: bool = False,
**kwargs,
) -> Iterable[str]:
exp_ref_set: set["ExpRefInfo"] = set()
exp_ref_set: set[ExpRefInfo] = set()
if all_commits:
exp_ref_set.update(exp_refs(repo.scm, git_remote))
elif exp_names:
@@ -55,7 +55,7 @@ def pull( # noqa: C901
rev_dict = iter_revs(repo.scm, rev, num)
rev_set = set(rev_dict.keys())
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
for _, ref_info_list in ref_info_dict.items():
for ref_info_list in ref_info_dict.values():
exp_ref_set.update(ref_info_list)
pull_result = _pull(repo, git_remote, exp_ref_set, force)
@@ -97,7 +97,7 @@ def _pull(
def group_result(refspec):
return results[str(refspec)]
pull_result: Mapping[SyncStatus, list["ExpRefInfo"]] = group_by(group_result, refs)
pull_result: Mapping[SyncStatus, list[ExpRefInfo]] = group_by(group_result, refs)
return pull_result
+3 -3
View File
@@ -78,7 +78,7 @@ def exp_refs_from_rev(scm: "Git", rev: list[str], num: int = 1) -> set["ExpRefIn
rev_dict = iter_revs(scm, rev, num)
rev_set = set(rev_dict.keys())
ref_info_dict = exp_refs_by_baseline(scm, rev_set)
for _, ref_info_list in ref_info_dict.items():
for ref_info_list in ref_info_dict.values():
exp_ref_set.update(ref_info_list)
return exp_ref_set
@@ -96,7 +96,7 @@ def push(
push_cache: bool = False,
**kwargs: Any,
) -> dict[str, Any]:
exp_ref_set: set["ExpRefInfo"] = set()
exp_ref_set: set[ExpRefInfo] = set()
assert isinstance(repo.scm, Git)
if all_commits:
exp_ref_set.update(exp_refs(repo.scm))
@@ -162,7 +162,7 @@ def _push(
def group_result(refspec):
return results[str(refspec)]
pull_result: Mapping[SyncStatus, list["ExpRefInfo"]] = group_by(group_result, refs)
pull_result: Mapping[SyncStatus, list[ExpRefInfo]] = group_by(group_result, refs)
return pull_result
+4 -4
View File
@@ -323,7 +323,7 @@ class LocalCeleryQueue(BaseStashQueue):
def _get_running_task_ids(self) -> set[str]:
running_task_ids: set[str] = set()
active_workers = self.worker_status()
for _, tasks in active_workers.items():
for tasks in active_workers.values():
task = first(tasks)
if task:
running_task_ids.add(task["id"])
@@ -470,11 +470,11 @@ class LocalCeleryQueue(BaseStashQueue):
exp_names = [exp_names]
results: dict[str, ExpRefAndQueueEntry] = {}
exp_ref_match: dict[str, Optional["ExpRefInfo"]] = resolve_name(
exp_ref_match: dict[str, Optional[ExpRefInfo]] = resolve_name(
self.scm, exp_names, git_remote
)
if not git_remote:
queue_entry_match: dict[str, Optional["QueueEntry"]] = (
queue_entry_match: dict[str, Optional[QueueEntry]] = (
self.match_queue_entry_by_name(
exp_names, self.iter_queued(), self.iter_done()
)
@@ -613,7 +613,7 @@ class LocalCeleryQueue(BaseStashQueue):
"""Map exp refs to any available successful executors."""
from dvc.repo.experiments.serialize import ExpExecutor, LocalExpExecutor
result: dict[str, "ExpExecutor"] = {}
result: dict[str, ExpExecutor] = {}
for entry, exec_result in self.iter_success():
if baseline_revs and entry.baseline_rev not in baseline_revs:
continue
+9 -9
View File
@@ -21,11 +21,11 @@ def remove_tasks( # noqa: C901, PLR0912
"""
from celery.result import AsyncResult
stash_revs: dict[str, "ExpStashEntry"] = {}
failed_stash_revs: list["ExpStashEntry"] = []
done_entry_set: set["QueueEntry"] = set()
stash_revs: dict[str, ExpStashEntry] = {}
failed_stash_revs: list[ExpStashEntry] = []
done_entry_set: set[QueueEntry] = set()
stash_rev_all = celery_queue.stash.stash_revs
failed_rev_all: dict[str, "ExpStashEntry"] = {}
failed_rev_all: dict[str, ExpStashEntry] = {}
if celery_queue.failed_stash:
failed_rev_all = celery_queue.failed_stash.stash_revs
for entry in queue_entries:
@@ -90,17 +90,17 @@ def celery_clear(
"""
removed: list[str] = []
entry_list: list["QueueEntry"] = []
entry_list: list[QueueEntry] = []
if queued:
queue_entries: list["QueueEntry"] = list(self.iter_queued())
queue_entries: list[QueueEntry] = list(self.iter_queued())
entry_list.extend(queue_entries)
removed.extend(_get_names(queue_entries))
if failed:
failed_tasks: list["QueueDoneResult"] = list(self.iter_failed())
failed_tasks: list[QueueDoneResult] = list(self.iter_failed())
entry_list.extend([result.entry for result in failed_tasks])
removed.extend(_get_names(failed_tasks))
if success:
success_tasks: list["QueueDoneResult"] = list(self.iter_success())
success_tasks: list[QueueDoneResult] = list(self.iter_success())
entry_list.extend([result.entry for result in success_tasks])
removed.extend(_get_names(success_tasks))
@@ -125,7 +125,7 @@ def celery_remove(self: "LocalCeleryQueue", revs: Collection[str]) -> list[str]:
remained: list[str] = []
removed: list[str] = []
entry_to_remove: list["QueueEntry"] = []
entry_to_remove: list[QueueEntry] = []
for name, entry in match_results.items():
if entry:
entry_to_remove.append(entry)
+1 -1
View File
@@ -125,7 +125,7 @@ class TempDirQueue(WorkspaceQueue):
)
except DvcException:
raise
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'") from exc
finally:
executor.cleanup(infofile)
+1 -1
View File
@@ -25,7 +25,7 @@ def get_remote_executor_refs(scm: "Git", remote_url: str) -> list[str]:
refs = []
for ref in iter_remote_refs(scm, remote_url, base=EXPS_NAMESPACE):
if not ref.startswith(EXEC_NAMESPACE) and ref != EXPS_STASH:
refs.append(ref)
refs.append(ref) # noqa: PERF401
return refs
+1 -1
View File
@@ -131,7 +131,7 @@ class WorkspaceQueue(BaseStashQueue):
)
except DvcException:
raise
except Exception as exc: # noqa: BLE001
except Exception as exc:
raise DvcException(f"Failed to reproduce experiment '{rev[:7]}'") from exc
finally:
executor.cleanup(infofile)
+6 -6
View File
@@ -34,17 +34,17 @@ def remove( # noqa: C901, PLR0912
removed: list[str] = []
if not any([exp_names, queue, all_commits, rev]):
return removed
celery_queue: "LocalCeleryQueue" = repo.experiments.celery_queue
celery_queue: LocalCeleryQueue = repo.experiments.celery_queue
if queue:
removed.extend(celery_queue.clear(queued=True))
assert isinstance(repo.scm, Git)
exp_ref_list: list["ExpRefInfo"] = []
queue_entry_list: list["QueueEntry"] = []
exp_ref_list: list[ExpRefInfo] = []
queue_entry_list: list[QueueEntry] = []
if exp_names:
results: dict[str, "ExpRefAndQueueEntry"] = (
results: dict[str, ExpRefAndQueueEntry] = (
celery_queue.get_ref_and_entry_by_names(exp_names, git_remote)
)
remained: list[str] = []
@@ -94,11 +94,11 @@ def _resolve_exp_by_baseline(
) -> dict[str, "ExpRefInfo"]:
assert isinstance(repo.scm, Git)
commit_ref_dict: dict[str, "ExpRefInfo"] = {}
commit_ref_dict: dict[str, ExpRefInfo] = {}
rev_dict = iter_revs(repo.scm, rev, num)
rev_set = set(rev_dict.keys())
ref_info_dict = exp_refs_by_baseline(repo.scm, rev_set, git_remote)
for _, ref_info_list in ref_info_dict.items():
for ref_info_list in ref_info_dict.values():
for ref_info in ref_info_list:
commit_ref_dict[ref_info.name] = ref_info
return commit_ref_dict
+10 -2
View File
@@ -50,7 +50,12 @@ class SerializableExp:
Params, metrics, deps, outs are filled via repo fs/index, all other fields
should be passed via kwargs.
"""
from dvc.dependency import ParamsDependency, RepoDependency
from dvc.dependency import (
DatasetDependency,
DbDependency,
ParamsDependency,
RepoDependency,
)
rev = rev or repo.get_rev()
assert rev
@@ -68,7 +73,10 @@ class SerializableExp:
nfiles=dep.meta.nfiles if dep.meta else None,
)
for dep in repo.index.deps
if not isinstance(dep, (ParamsDependency, RepoDependency))
if not isinstance(
dep,
(ParamsDependency, RepoDependency, DatasetDependency, DbDependency),
)
},
outs={
relpath(out.fs_path, repo.root_dir): ExpOut(
+2 -2
View File
@@ -117,7 +117,7 @@ def _build_rows(
**kwargs,
) -> Iterator[tuple["CellT", ...]]:
for baseline in baseline_states:
row: dict[str, "CellT"] = dict.fromkeys(all_headers, fill_value)
row: dict[str, CellT] = dict.fromkeys(all_headers, fill_value)
row["Experiment"] = ""
if baseline.name:
row["rev"] = baseline.name
@@ -232,7 +232,7 @@ def _exp_range_rows(
logger.debug("Returning tip commit for legacy checkpoint exp")
exp = first(exp_range.revs)
if exp:
row: dict[str, "CellT"] = dict.fromkeys(all_headers, fill_value)
row: dict[str, CellT] = dict.fromkeys(all_headers, fill_value)
row["Experiment"] = exp.name or ""
row["rev"] = exp.rev[:7] if Git.is_sha(exp.rev) else exp.rev
row["typ"] = "branch_base" if is_base else "branch_commit"
+1 -1
View File
@@ -199,7 +199,7 @@ def exp_commits(
scm: "Git", ref_infos: Optional[Iterable[ExpRefInfo]] = None
) -> Iterable[str]:
"""Iterate over all experiment commits."""
shas: set["str"] = set()
shas: set[str] = set()
refs = ref_infos if ref_infos else exp_refs(scm)
for ref_info in refs:
shas.update(scm.branch_revs(str(ref_info), ref_info.baseline_sha))
+4 -1
View File
@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
from dvc.exceptions import DownloadError
from dvc.log import logger
from dvc.stage.cache import RunCacheNotSupported
from dvc.ui import ui
from dvc_data.index import DataIndex, FileStorage
@@ -136,6 +137,8 @@ def fetch( # noqa: PLR0913
try:
if run_cache:
self.stage_cache.pull(remote)
except RunCacheNotSupported as e:
logger.debug("failed to pull run cache: %s", e)
except DownloadError as exc:
failed_count += exc.amount
@@ -200,7 +203,7 @@ def fetch( # noqa: PLR0913
def _log_unversioned(data: list["DataIndex"]) -> tuple[list["DataIndex"], int]:
ret: list["DataIndex"] = []
ret: list[DataIndex] = []
unversioned: list[str] = []
for fs_index in data:
remote = fs_index.storage_map[()].remote
+2 -2
View File
@@ -50,7 +50,7 @@ def _used_obj_ids_not_in_remote(
@locked
def gc( # noqa: PLR0913, C901
def gc( # noqa: C901, PLR0912, PLR0913
self: "Repo",
all_branches: bool = False,
cloud: bool = False,
@@ -96,7 +96,7 @@ def gc( # noqa: PLR0913, C901
repos = []
all_repos = [Repo(path) for path in repos]
odb_to_obj_ids: "ObjectContainer" = {}
odb_to_obj_ids: ObjectContainer = {}
with ExitStack() as stack:
for repo in all_repos:
stack.enter_context(repo.lock)
+1 -1
View File
@@ -54,7 +54,7 @@ def get(
from dvc.fs import download
from dvc.fs.data import DataFileSystem
fs: Union[DataFileSystem, "DVCFileSystem"]
fs: Union[DataFileSystem, DVCFileSystem]
if os.path.isabs(path):
fs = DataFileSystem(index=repo.index.data["local"])
fs_path = fs.from_os_path(path)
+1 -1
View File
@@ -22,7 +22,7 @@ def check_acyclic(graph: "DiGraph") -> None:
except nx.NetworkXNoCycle:
return
stages: set["Stage"] = set()
stages: set[Stage] = set()
for from_node, to_node, _ in edges:
stages.add(from_node)
stages.add(to_node)
-6
View File
@@ -4,7 +4,6 @@ from funcy import compact
from dvc.exceptions import OutputDuplicationError
from dvc.repo.scm_context import scm_context
from dvc.ui import ui
from dvc.utils import resolve_output, resolve_paths
if TYPE_CHECKING:
@@ -25,11 +24,6 @@ def imp_db(
force: bool = False,
connection: Optional[str] = None,
):
ui.warn("WARNING: import-db is an experimental feature.")
ui.warn(
"The functionality may change or break without notice, "
"which could lead to unexpected behavior."
)
assert sql or table
assert output_format in ("csv", "json")
+13 -13
View File
@@ -300,7 +300,7 @@ class Index:
self._artifacts = artifacts or {}
self._datasets: dict[str, list[dict[str, Any]]] = datasets or {}
self._datasets_lock: dict[str, list[dict[str, Any]]] = datasets_lock or {}
self._collected_targets: dict[int, list["StageInfo"]] = {}
self._collected_targets: dict[int, list[StageInfo]] = {}
@cached_property
def rev(self) -> Optional[str]:
@@ -416,7 +416,7 @@ class Index:
@cached_property
def out_data_keys(self) -> dict[str, set["DataIndexKey"]]:
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
by_workspace["repo"] = set()
by_workspace["local"] = set()
@@ -470,7 +470,7 @@ class Index:
@cached_property
def data_keys(self) -> dict[str, set["DataIndexKey"]]:
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
by_workspace["repo"] = set()
by_workspace["local"] = set()
@@ -488,7 +488,7 @@ class Index:
def metric_keys(self) -> dict[str, set["DataIndexKey"]]:
from .metrics.show import _collect_top_level_metrics
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
by_workspace["repo"] = set()
@@ -509,7 +509,7 @@ class Index:
def param_keys(self) -> dict[str, set["DataIndexKey"]]:
from .params.show import _collect_top_level_params
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
by_workspace["repo"] = set()
param_paths = _collect_top_level_params(self.repo)
@@ -525,7 +525,7 @@ class Index:
@cached_property
def plot_keys(self) -> dict[str, set["DataIndexKey"]]:
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
by_workspace["repo"] = set()
@@ -548,7 +548,7 @@ class Index:
@cached_property
def data(self) -> "dict[str, DataIndex]":
prefix: "DataIndexKey"
prefix: DataIndexKey
loaded = False
index = self.repo.data_index
@@ -600,7 +600,7 @@ class Index:
if not onerror:
def onerror(_target, _exc):
raise
raise # noqa: PLE0704
targets = ensure_list(targets)
if not targets:
@@ -627,7 +627,7 @@ class Index:
jobs: Optional[int] = None,
push: bool = False,
) -> "ObjectContainer":
used: "ObjectContainer" = defaultdict(set)
used: ObjectContainer = defaultdict(set)
pairs = self.collect_targets(targets, recursive=recursive, with_deps=with_deps)
for stage, filter_info in pairs:
for odb, objs in stage.get_used_objs(
@@ -748,7 +748,7 @@ class IndexView:
@cached_property
def out_data_keys(self) -> dict[str, set["DataIndexKey"]]:
by_workspace: dict[str, set["DataIndexKey"]] = defaultdict(set)
by_workspace: dict[str, set[DataIndexKey]] = defaultdict(set)
by_workspace["repo"] = set()
by_workspace["local"] = set()
@@ -764,7 +764,7 @@ class IndexView:
@cached_property
def _data_prefixes(self) -> dict[str, "_DataPrefixes"]:
prefixes: dict[str, "_DataPrefixes"] = defaultdict(
prefixes: dict[str, _DataPrefixes] = defaultdict(
lambda: _DataPrefixes(set(), set())
)
for out, filter_info in self._filtered_outs:
@@ -781,7 +781,7 @@ class IndexView:
@cached_property
def data_keys(self) -> dict[str, set["DataIndexKey"]]:
ret: dict[str, set["DataIndexKey"]] = defaultdict(set)
ret: dict[str, set[DataIndexKey]] = defaultdict(set)
for out, filter_info in self._filtered_outs:
if not out.use_cache:
@@ -811,7 +811,7 @@ class IndexView:
except KeyError:
return False
data: dict[str, Union["DataIndex", "DataIndexView"]] = {}
data: dict[str, Union[DataIndex, DataIndexView]] = {}
for workspace, data_index in self._index.data.items():
if self.stages:
data[workspace] = view(data_index, partial(key_filter, workspace))
+1 -1
View File
@@ -77,7 +77,7 @@ def _ls(
recursive: Optional[bool] = None,
dvc_only: bool = False,
):
fs: "DVCFileSystem" = repo.dvcfs
fs: DVCFileSystem = repo.dvcfs
fs_path = fs.from_os_path(path)
fs_path = fs.info(fs_path)["name"]
+20 -3
View File
@@ -1,3 +1,4 @@
import logging
import os
from collections.abc import Iterable, Iterator
from itertools import chain
@@ -8,7 +9,7 @@ from scmrepo.exceptions import SCMError
from dvc.log import logger
from dvc.scm import NoSCMError
from dvc.utils import as_posix, expand_paths
from dvc.utils import as_posix
from dvc.utils.collections import ensure_list
from dvc.utils.serialize import load_path
@@ -103,7 +104,7 @@ def _collect_metrics(
paths = (fs.from_os_path(metric) for metric in metrics)
# make paths absolute for DVCFileSystem
repo_paths = (f"{fs.root_marker}{path}" for path in paths)
return ldistinct(expand_paths(fs, repo_paths))
return ldistinct(try_expand_paths(fs, repo_paths))
class FileResult(TypedDict, total=False):
@@ -116,6 +117,22 @@ class Result(TypedDict, total=False):
error: Exception
def try_expand_paths(fs: "FileSystem", paths: Iterable[str]) -> Iterator[str]:
for path in paths:
try:
if fs.isdir(path):
yield from fs.find(path)
continue
except Exception as e: # noqa: BLE001
logger.debug(
"failed to expand %r: %s",
path,
e,
exc_info=logger.isEnabledFor(logging.TRACE), # type: ignore[attr-defined]
)
yield path
def to_relpath(fs: "FileSystem", root_dir: str, d: Result) -> Result:
relpath = fs.relpath
cwd = fs.getcwd()
@@ -207,7 +224,7 @@ def show(
on_error=on_error,
)
res[rev] = Result(data=result)
except Exception as exc: # noqa: BLE001
except Exception as exc:
if on_error == "raise":
raise
+1 -1
View File
@@ -146,7 +146,7 @@ def _cached_clone(url, rev):
@wrap_with(threading.Lock())
def _clone_default_branch(url, rev): # noqa: PLR0912
def _clone_default_branch(url, rev):
"""Get or create a clean clone of the url.
The cloned is reactualized with git pull unless rev is a known sha.
+4 -4
View File
@@ -6,9 +6,9 @@ from typing import TYPE_CHECKING, Any, Optional, Union
from dvc.dependency.param import ParamsDependency, read_param_file
from dvc.log import logger
from dvc.repo.metrics.show import FileResult, Result
from dvc.repo.metrics.show import FileResult, Result, try_expand_paths
from dvc.stage import PipelineStage
from dvc.utils import as_posix, expand_paths
from dvc.utils import as_posix
from dvc.utils.collections import ensure_list
if TYPE_CHECKING:
@@ -77,7 +77,7 @@ def _collect_params(
path = fs.from_os_path(param)
# make paths absolute for DVCFileSystem
repo_path = f"{fs.root_marker}{path}"
ret.update(dict.fromkeys(expand_paths(fs, [repo_path]), _params))
ret.update(dict.fromkeys(try_expand_paths(fs, [repo_path]), _params))
return ret
@@ -191,7 +191,7 @@ def show(
on_error=on_error,
)
res[rev] = Result(data=params)
except Exception as exc: # noqa: BLE001
except Exception as exc:
if on_error == "raise":
raise
logger.warning("failed to load params in revision %r, %s", rev, str(exc))
+9 -2
View File
@@ -1,10 +1,14 @@
from contextlib import suppress
from dvc.exceptions import InvalidArgumentError, UploadError
from dvc.log import logger
from dvc.stage.cache import RunCacheNotSupported
from dvc.ui import ui
from . import locked
logger = logger.getChild(__name__)
def _rebuild(idx, path, fs, cb):
from dvc_data.index import DataIndex, DataIndexEntry, Meta
@@ -95,8 +99,11 @@ def push( # noqa: PLR0913
"Multiple rev push is unsupported for cloud versioned remotes"
)
used_run_cache = self.stage_cache.push(remote) if run_cache else []
transferred_count += len(used_run_cache)
try:
used_run_cache = self.stage_cache.push(remote) if run_cache else []
transferred_count += len(used_run_cache)
except RunCacheNotSupported as e:
logger.debug("failed to push run cache: %s", e)
if isinstance(targets, str):
targets = [targets]
+6 -6
View File
@@ -29,7 +29,7 @@ def collect_stages(
recursive: bool = False,
glob: bool = False,
) -> list["Stage"]:
stages: list["Stage"] = []
stages: list[Stage] = []
for target in targets:
stages.extend(repo.stage.collect(target, recursive=recursive, glob=glob))
return ldistinct(stages)
@@ -166,10 +166,10 @@ def _reproduce(
) -> list["Stage"]:
assert on_error in ("fail", "keep-going", "ignore")
result: list["Stage"] = []
failed: list["Stage"] = []
to_skip: dict["Stage", "Stage"] = {}
ret: Optional["Stage"] = None
result: list[Stage] = []
failed: list[Stage] = []
to_skip: dict[Stage, Stage] = {}
ret: Optional[Stage] = None
force_state = dict.fromkeys(stages, force)
@@ -228,7 +228,7 @@ def reproduce(
if not kwargs.get("interactive", False):
kwargs["interactive"] = self.config["core"].get("interactive", False)
stages: list["Stage"] = []
stages: list[Stage] = []
if not all_pipelines:
targets_list = ensure_list(targets or PROJECT_FILE)
stages = collect_stages(self, targets_list, recursive=recursive, glob=glob)
+1 -1
View File
@@ -21,7 +21,7 @@ class SCMContext:
def __init__(self, scm: "Base", config: Optional[dict[str, Any]] = None) -> None:
from funcy import get_in
self.scm: "Base" = scm
self.scm: Base = scm
self.autostage: bool = get_in(
config or {}, ["core", "autostage"], default=False
)
+1 -1
View File
@@ -108,7 +108,7 @@ def locked(f):
class StageLoad:
def __init__(self, repo: "Repo") -> None:
self.repo: "Repo" = repo
self.repo: Repo = repo
@property
def fs(self):
+1 -1
View File
@@ -34,7 +34,7 @@ def update( # noqa: C901
raise InvalidArgumentError("--remote can't be used without --to-remote")
import_stages = set()
other_stage_infos: list["StageInfo"] = []
other_stage_infos: list[StageInfo] = []
for stage_info in self.index.collect_targets(targets, recursive=recursive):
if stage_info.stage.is_import:
+1 -1
View File
@@ -166,7 +166,7 @@ def update_worktree_stages(repo: "Repo", stage_infos: Iterable["StageInfo"]):
view = IndexView(repo.index, stage_infos, outs_filter=outs_filter)
local_index = view.data["repo"]
remote_indexes: dict[str, tuple["Remote", "DataIndex"]] = {}
remote_indexes: dict[str, tuple[Remote, DataIndex]] = {}
for stage in view.stages:
for out in stage.outs:
_update_worktree_out(repo, out, local_index, remote_indexes)
+21 -16
View File
@@ -290,7 +290,7 @@ class Stage(params.StageParams):
)
def short_description(self) -> Optional["str"]:
desc: Optional["str"] = None
desc: Optional[str] = None
if self.desc:
with suppress(ValueError):
# try to use first non-empty line as a description
@@ -339,7 +339,7 @@ class Stage(params.StageParams):
for out in self.outs:
status = out.status()
if status:
if allow_missing and status[str(out)] == "not in cache":
if allow_missing and status[str(out)] in ["not in cache", "deleted"]:
continue
logger.debug(
"Output '%s' of %s changed because it is '%s'.",
@@ -418,17 +418,28 @@ class Stage(params.StageParams):
@rwlocked(read=["deps"], write=["outs"])
def reproduce(self, interactive=False, **kwargs) -> Optional["Stage"]:
if not (
kwargs.get("force", False)
or self.changed(
kwargs.get("allow_missing", False), kwargs.pop("upstream", None)
)
):
force = kwargs.get("force", False)
allow_missing = kwargs.get("allow_missing", False)
pull = kwargs.get("pull", False)
upstream = kwargs.pop("upstream", None)
if force:
pass
# Skip stages with missing data if otherwise unchanged
elif not self.changed(allow_missing, upstream):
if not isinstance(self, PipelineStage) and self.is_data_source:
logger.info("'%s' didn't change, skipping", self.addressing)
else:
logger.info("Stage '%s' didn't change, skipping", self.addressing)
return None
# Pull stages with missing data if otherwise unchanged
elif not self.changed(True, upstream) and pull:
try:
logger.info("Pulling data for %s", self)
self.repo.pull(self.addressing, jobs=kwargs.get("jobs", None))
self.checkout()
return None
except CheckoutError:
logger.info("Unable to pull data for %s", self)
msg = f"Going to reproduce {self}. Are you sure you want to continue?"
if interactive and not prompt.confirm(msg):
@@ -597,16 +608,10 @@ class Stage(params.StageParams):
if (self.cmd or self.is_import) and not self.frozen and not dry:
self.remove_outs(ignore_remove=False, force=False)
if (
self.is_import and (not self.frozen or kwargs.get("pull"))
) or self.is_partial_import:
if (self.is_import and not self.frozen) or self.is_partial_import:
self._sync_import(dry, force, kwargs.get("jobs", None), no_download)
elif not self.frozen and self.cmd:
self._run_stage(dry, force, **kwargs)
elif kwargs.get("pull"):
logger.info("Pulling data for %s", self)
self.repo.pull(self.addressing, jobs=kwargs.get("jobs", None))
self.checkout()
elif not dry:
args = ("outputs", "frozen ") if self.frozen else ("data sources", "")
logger.info("Verifying %s in %s%s", *args, self)
@@ -653,7 +658,7 @@ class Stage(params.StageParams):
def checkout(
self, allow_missing: bool = False, **kwargs
) -> dict[str, list["StrPath"]]:
stats: dict[str, list["StrPath"]] = defaultdict(list)
stats: dict[str, list[StrPath]] = defaultdict(list)
if self.is_partial_import:
return stats
+3 -2
View File
@@ -229,7 +229,7 @@ class StageCache:
if not dry:
cached_stage.checkout()
def transfer(self, from_odb, to_odb):
def transfer(self, from_odb, to_odb, force=True):
from dvc.fs import HTTPFileSystem, LocalFileSystem
from dvc.fs.callbacks import TqdmCallback
@@ -258,10 +258,11 @@ class StageCache:
dst = to_fs.join(to_odb.path, rel)
key = to_fs.parent(dst)
# check if any build cache already exists for this key
# TODO: check if MaxKeys=1 or something like that applies
# or otherwise this will take a lot of time!
if to_fs.exists(key) and first(to_fs.find(key)):
if not force and to_fs.exists(key) and first(to_fs.find(key)):
continue
src_name = from_fs.name(src)
+1 -1
View File
@@ -50,7 +50,7 @@ class StageLoader(Mapping):
from dvc.output import Output, merge_file_meta_from_cloud
assert isinstance(lock_data, dict)
items: Iterable[tuple[str, "Output"]] = chain(
items: Iterable[tuple[str, Output]] = chain(
((StageParams.PARAM_DEPS, dep) for dep in stage.deps),
((StageParams.PARAM_OUTS, out) for out in stage.outs),
)
+57 -16
View File
@@ -2,6 +2,9 @@ import os
import signal
import subprocess
import threading
from functools import cache
from packaging.version import InvalidVersion, Version
from dvc.log import logger
from dvc.utils import fix_env
@@ -12,25 +15,59 @@ from .exceptions import StageCmdFailedError
logger = logger.getChild(__name__)
@cache
def _fish_supports_no_config(executable) -> bool:
"""
Check if the fish shell supports the --no-config option.
Parameters:
executable (str): The path to the fish shell executable.
Returns:
bool: True if the fish version is greater than 3.3.0, False otherwise.
"""
try:
output = subprocess.check_output( # noqa: S603
[executable, "--version"],
text=True,
)
version = Version(output.split(" ")[-1].strip())
version_to_check = Version("3.3.0")
return version >= version_to_check
except (subprocess.CalledProcessError, IndexError, InvalidVersion):
logger.trace("could not check fish version, defaulting to False")
return False
def _warn_if_fish(executable):
if (
executable is None
or os.path.basename(executable) != "fish"
or _fish_supports_no_config(executable)
):
return
logger.warning(
"DVC detected that you are using a version of fish shell below 3.3.0 "
"Be aware that it might cause problems by overwriting "
"your current environment variables with values defined "
"in 'config.fish', which might affect your command. See "
"https://github.com/iterative/dvc/issues/1307. "
)
def _make_cmd(executable, cmd):
if executable is None:
return cmd
opts = {"zsh": ["--no-rcs"], "bash": ["--noprofile", "--norc"]}
opts = {
"zsh": ["--no-rcs"],
"bash": ["--noprofile", "--norc"],
"fish": [],
}
name = os.path.basename(executable).lower()
return [executable, *opts.get(name, []), "-c", cmd]
def warn_if_fish(executable):
if executable is None or os.path.basename(os.path.realpath(executable)) != "fish":
return
logger.warning(
"DVC detected that you are using fish as your default "
"shell. Be aware that it might cause problems by overwriting "
"your current environment variables with values defined "
"in '.fishrc', which might affect your command. See "
"https://github.com/iterative/dvc/issues/1307. "
)
opt = opts.get(name, [])
if name == "fish" and _fish_supports_no_config(executable):
opt.append("--no-config")
return [executable, *opt, "-c", cmd]
def _enforce_cmd_list(cmd):
@@ -106,7 +143,7 @@ def cmd_run(stage, dry=False, run_env=None):
executable = get_executable()
if not dry:
warn_if_fish(executable)
_warn_if_fish(executable)
for cmd in commands:
display_command(cmd)
@@ -117,7 +154,11 @@ def cmd_run(stage, dry=False, run_env=None):
def _pull_missing_deps(stage):
from dvc.dependency import DatasetDependency, DbDependency
for dep in stage.deps:
if isinstance(dep, (DatasetDependency, DbDependency)):
continue
if not dep.exists:
stage.repo.pull(dep.def_path)
+2 -2
View File
@@ -302,9 +302,9 @@ def _get_stage_files(stage: "Stage") -> list[str]:
and dep.is_in_repo
and not stage.repo.dvcfs.isdvc(stage.repo.dvcfs.from_os_path(str(dep)))
):
ret.append(relpath(dep.fs_path))
ret.append(relpath(dep.fs_path)) # noqa: PERF401
for out in stage.outs:
if not out.use_scm_ignore and out.is_in_repo:
ret.append(relpath(out.fs_path))
ret.append(relpath(out.fs_path)) # noqa: PERF401
return ret
@@ -1,2 +1,19 @@
def test_add(bench_dvc, tmp_dir, dvc, dataset):
bench_dvc("add", dataset)
from .test_checkout import _skip_unsupported_link
def generate_test(*, link_type="copy"):
def _test_add(bench_dvc, tmp_dir, dvc, dataset):
_skip_unsupported_link((tmp_dir / ".dvc" / "cache"), tmp_dir, link_type)
with dvc.config.edit() as conf:
conf["cache"]["type"] = link_type
bench_dvc("add", dataset)
bench_dvc("add", dataset, name="noop")
return _test_add
test_add_copy = generate_test(link_type="copy")
test_add_symlink = generate_test(link_type="symlink")
test_add_hardlink = generate_test(link_type="hardlink")
@@ -1,9 +1,5 @@
from shutil import rmtree
import pytest
pytestmark = pytest.mark.requires(minversion=(2, 15, 0), reason="new command")
def test_data_status(bench_dvc, tmp_dir, scm, dvc, make_dataset):
args = ("data", "status")
@@ -1,7 +1,3 @@
import pytest
@pytest.mark.requires(minversion=(2, 28, 0))
def test_exp_show(make_project, monkeypatch, bench_dvc, dvc_bin):
url = "https://github.com/iterative/example-get-started"
rev = "main"
@@ -2,6 +2,12 @@ import pytest
@pytest.mark.flaky(reruns=3)
@pytest.mark.requires(
"!=3.53.*,!=3.54.0",
reason="Takes 10 mins to run. Regression in 3.53.0, fixed in 3.54.1",
)
# Introduced in https://github.com/iterative/dvc/pull/10388.
# Fixed in https://github.com/iterative/dvc/pull/10531.
def test_import(bench_dvc, tmp_dir, scm, dvc, make_dataset, remote):
dataset = make_dataset(
cache=False, files=False, dvcfile=True, commit=True, remote=True
@@ -1,10 +1,7 @@
import pytest
from dvc.repo import Repo
from dvc.testing.benchmarks.fixtures import _pull
@pytest.mark.requires(minversion=(2, 34, 0), reason="top-level plots not supported")
def test_plots(project, bench_dvc):
with Repo() as dvc:
_pull(dvc)
@@ -1,9 +1,8 @@
import shutil
def test_sharing(bench_dvc, tmp_dir, dvc, dataset, remote):
bench_dvc("add", dataset)
bench_dvc("add", dataset, name="noop")
def test_sharing(bench_dvc, tmp_dir, dvc, make_dataset, remote):
dataset = make_dataset(cache=True, dvcfile=True)
bench_dvc("push")
bench_dvc("push", name="noop")
@@ -13,6 +12,3 @@ def test_sharing(bench_dvc, tmp_dir, dvc, dataset, remote):
bench_dvc("fetch")
bench_dvc("fetch", name="noop")
bench_dvc("checkout")
bench_dvc("checkout", name="noop")
+2
View File
@@ -1 +1,3 @@
from .fixtures import * # noqa: F403
pytest_plugins = ["dvc.testing.plugin"]
+52 -41
View File
@@ -1,7 +1,8 @@
import os
import shutil
import sys
from pathlib import Path
from subprocess import check_output
from subprocess import check_call, check_output
from typing import Optional
import pytest
@@ -23,13 +24,14 @@ class VirtualEnv:
self.bin = self.path / ("Scripts" if os.name == "nt" else "bin")
def create(self) -> None:
import virtualenv
check_call([sys.executable, "-m", "uv", "venv", self.path]) # noqa:S603
virtualenv.cli_run([os.fspath(self.path)])
def install(self, *packages: str) -> None:
check_call([sys.executable, "-m", "uv", "pip", "install", *packages]) # noqa: S603
def run(self, cmd: str, *args: str, env: Optional[dict[str, str]] = None) -> None:
exe = self.which(cmd)
check_output([exe, *args], env=env) # noqa: S603
check_call([exe, *args], env=env) # noqa: S603
def which(self, cmd: str) -> str:
assert self.bin.exists()
@@ -58,8 +60,8 @@ def dvc_venvs():
@pytest.fixture(scope="session")
def dvc_git_repo(tmp_path_factory, bench_config):
url = bench_config.dvc_git_repo
def dvc_repo(tmp_path_factory, bench_config):
url = bench_config.dvc_repo
if os.path.isdir(url):
return url
@@ -71,8 +73,10 @@ def dvc_git_repo(tmp_path_factory, bench_config):
@pytest.fixture(scope="session")
def dvc_bench_git_repo(tmp_path_factory, bench_config):
url = bench_config.dvc_bench_git_repo
def dvc_bench_repo(tmp_path_factory, bench_config):
url = bench_config.dvc_bench_repo
if url is None:
pytest.skip("--dvc-bench-repo is not set")
if os.path.isdir(url):
return Path(url)
@@ -88,56 +92,55 @@ def make_dvc_bin(
dvc_rev,
dvc_venvs,
make_dvc_venv,
dvc_git_repo,
dvc_repo,
bench_config,
request,
):
if dvc_rev:
venv = dvc_venvs.get(dvc_rev)
venv: VirtualEnv = dvc_venvs.get(dvc_rev)
if not venv:
venv = make_dvc_venv(dvc_rev)
venv.run("pip", "install", "-U", "pip")
if bench_config.dvc_install_deps:
egg = f"dvc[{bench_config.dvc_install_deps}]"
pkg = f"dvc[{bench_config.dvc_install_deps}]"
else:
egg = "dvc"
venv.run("pip", "install", f"git+file://{dvc_git_repo}@{dvc_rev}#egg={egg}")
if dvc_rev in ["2.18.1", "2.11.0", "2.6.3"]:
venv.run("pip", "install", "fsspec==2022.11.0")
pkg = "dvc"
packages = [f"{pkg} @ git+file://{dvc_repo}@{dvc_rev}"]
try:
if version.Version(dvc_rev) < version.Version("3.50.3"):
packages.append("pygit2==1.14.1")
except version.InvalidVersion:
pass
venv.install(*packages)
dvc_venvs[dvc_rev] = venv
dvc_bin = venv.which("dvc")
else:
dvc_bin = bench_config.dvc_bin
def _dvc_bin(*args):
return check_output([dvc_bin, *args], text=True) # noqa: S603
check_call([dvc_bin, *args]) # noqa: S603
_dvc_bin.version = parse_tuple(_dvc_bin("--version")) # type: ignore[attr-defined]
_dvc_bin.version = check_output([dvc_bin, "--version"], text=True) # type: ignore[attr-defined] # noqa: S603
return _dvc_bin
def parse_tuple(version_string):
from packaging.version import Version
parsed = version.parse(version_string)
assert isinstance(parsed, Version)
return (parsed.major, parsed.minor, parsed.micro)
@pytest.fixture
def dvc_bin(request, make_dvc_bin):
if marker := request.node.get_closest_marker("requires"):
minversion = marker.kwargs.get("minversion") or first(marker.args)
assert minversion, (
"'minversion' needs to be specified as"
" a positional or a keyword argument"
)
reason = marker.kwargs.get("reason", "")
if isinstance(minversion, str):
minversion = parse_tuple(minversion)
if make_dvc_bin.version < minversion:
version_repr = ".".join(map(str, minversion))
pytest.skip(f"requires dvc>={version_repr}: {reason}")
from packaging.specifiers import SpecifierSet
from packaging.version import Version, parse
spec = first(marker.args)
assert spec is not None
spec = SpecifierSet(spec) if isinstance(spec, str) else spec
reason = marker.kwargs["reason"]
dvc_version = make_dvc_bin.version
version = Version(parse(dvc_version).base_version)
if version not in spec:
pytest.skip(
f"Version {dvc_version} "
f"does not satisfy requirement {spec!r}: {reason}"
)
return make_dvc_bin
@@ -164,11 +167,19 @@ def make_bench(request):
@pytest.fixture
def bench_dvc(dvc_bin, make_bench):
def bench_dvc(request, dvc_bin, make_bench):
def _bench_dvc(*args, **kwargs):
name = kwargs.pop("name", None)
name = f"-{name}" if name else ""
bench = make_bench(args[0] + name)
if request.config.getoption("--benchmark-cprofile-dump") or kwargs.pop(
"cprofile", False
):
cprofile_results = request.config.invocation_params.dir / "prof"
cprofile_results.mkdir(exist_ok=True)
stats_file = cprofile_results / f"{bench.name}.prof"
args = (*args, "--cprofile-dump", stats_file)
return bench.pedantic(dvc_bin, args=args, **kwargs)
return _bench_dvc
@@ -185,14 +196,14 @@ def _pull(repo, *args):
@pytest.fixture
def make_dataset(request, bench_config, tmp_dir, dvc_bench_git_repo):
def make_dataset(request, bench_config, tmp_dir, dvc_bench_repo):
def _make_dataset(
dvcfile=False, files=True, cache=False, commit=False, remote=False
):
from dvc.repo import Repo
path = tmp_dir / "dataset"
root = dvc_bench_git_repo
root = dvc_bench_repo
src = root / "data" / bench_config.dataset / "dataset"
src_dvc = src.with_suffix(".dvc")
@@ -251,7 +262,7 @@ def make_project(tmp_path_factory):
@pytest.fixture
def project(bench_config, monkeypatch, make_project):
rev = bench_config.project_rev
url = bench_config.project_git_repo
url = bench_config.project_repo
if os.path.isdir(url):
path = url
+63 -38
View File
@@ -1,57 +1,84 @@
import os
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Optional
DEFAULT_DVC_BIN = "dvc"
DEFAULT_DVC_GIT_REPO = "https://github.com/iterative/dvc"
DEFAULT_DVC_BENCH_GIT_REPO = "https://github.com/iterative/dvc-bench"
DEFAULT_PROJECT_GIT_REPO = "https://github.com/iterative/example-get-started"
DEFAULT_DVC_REPO = os.fspath(Path(__file__).parents[3])
DEFAULT_PROJECT_REPO = "https://github.com/iterative/example-get-started"
def pytest_report_header(config):
bconf = config.bench_config
return f"dvc-bench: (dataset: {bconf.dataset!r}, revs: {bconf.dvc_revs!r})"
return f"dvc-bench: {bconf}"
def pytest_generate_tests(metafunc):
str_revs = metafunc.config.getoption("--dvc-revs")
revs = str_revs.split(",") if str_revs else [None]
revs = metafunc.config.getoption("--dvc-revs")
if not revs:
revs = [None]
if "dvc_rev" in metafunc.fixturenames:
metafunc.parametrize("dvc_rev", revs, scope="session")
@dataclass
class DVCBenchConfig:
def __init__(self):
self.dataset = "small"
self.dvc_bin = DEFAULT_DVC_BIN
self.dvc_revs = None
self.dvc_git_repo = DEFAULT_DVC_GIT_REPO
self.dvc_install_deps = None
self.dvc_bench_git_repo = DEFAULT_DVC_BENCH_GIT_REPO
self.project_rev = None
self.project_git_repo = DEFAULT_PROJECT_GIT_REPO
dataset: str = "tiny"
dvc_repo: str = DEFAULT_DVC_REPO
dvc_bench_repo: Optional[str] = None
project_repo: str = DEFAULT_PROJECT_REPO
project_rev: Optional[str] = None
dvc_bin: str = DEFAULT_DVC_BIN
dvc_revs: Optional[list[str]] = None
dvc_install_deps: Optional[str] = None
def __repr__(self):
args = ", ".join(
f"{f.name}={val!r}"
for f in fields(self)
if (val := getattr(self, f.name)) != f.default
)
return f"{self.__class__.__name__}({args})"
def pytest_configure(config):
config.addinivalue_line(
"markers", "requires(minversion): mark a test as requiring minimum DVC version"
"markers",
"requires(spec): mark a test to run only on versions that satisfy the spec",
)
config.bench_config = DVCBenchConfig(
dataset=config.getoption("--dataset"),
dvc_repo=config.getoption("--dvc-repo"),
dvc_bench_repo=config.getoption("--dvc-bench-repo"),
project_repo=config.getoption("--project-repo"),
project_rev=config.getoption("--project-rev"),
dvc_bin=config.getoption("--dvc-bin"),
dvc_revs=config.getoption("--dvc-revs"),
dvc_install_deps=config.getoption("--dvc-install-deps"),
)
config.bench_config = DVCBenchConfig()
config.bench_config.dataset = config.getoption("--dataset")
config.bench_config.dvc_bin = config.getoption("--dvc-bin")
config.bench_config.dvc_revs = config.getoption("--dvc-revs")
config.bench_config.dvc_git_repo = config.getoption("--dvc-git-repo")
config.bench_config.dvc_install_deps = config.getoption("--dvc-install-deps")
config.bench_config.dvc_bench_git_repo = config.getoption("--dvc-bench-git-repo")
config.bench_config.project_rev = config.getoption("--project-rev")
config.bench_config.project_git_repo = config.getoption("--project-git-repo")
def resolve_path(path):
if os.path.isdir(path):
return os.path.abspath(path)
return path
def pytest_addoption(parser):
parser.addoption(
"--dataset",
type=str,
default="small",
default="tiny",
help="Dataset name to use in tests (e.g. tiny/small/large/mnist/etc)",
)
parser.addoption(
"--benchmark-cprofile-dump",
action="store_true",
default=False,
help="Save cprofile results",
)
parser.addoption(
"--dvc-bin",
type=str,
@@ -61,36 +88,34 @@ def pytest_addoption(parser):
parser.addoption(
"--dvc-revs",
type=str,
type=lambda revs: revs.split(","),
help=("Comma-separated list of DVC revisions to test (overrides `--dvc-bin`)"),
)
parser.addoption(
"--dvc-git-repo",
type=str,
default=DEFAULT_DVC_GIT_REPO,
"--dvc-repo",
type=resolve_path,
default=DEFAULT_DVC_REPO,
help="Path or url to dvc git repo",
)
parser.addoption(
"--dvc-install-deps",
type=str,
default="",
help="Comma-separated list of DVC installation packages",
)
parser.addoption(
"--dvc-bench-git-repo",
type=str,
default=DEFAULT_DVC_BENCH_GIT_REPO,
"--dvc-bench-repo",
type=resolve_path,
default=None,
help="Path or url to dvc-bench git repo (for loading benchmark datasets)",
)
parser.addoption("--project-rev", type=str, help="Project revision to test")
parser.addoption(
"--project-git-repo",
type=str,
default=DEFAULT_PROJECT_GIT_REPO,
"--project-repo",
type=resolve_path,
default=DEFAULT_PROJECT_REPO,
help="Path or url to dvc project",
)
+2 -2
View File
@@ -228,10 +228,10 @@ def docker_services(tmp_path_factory, request):
pytest.skip("disabled for Windows on CI")
try:
subprocess.check_output(
subprocess.check_output( # noqa: S602
"docker ps", # noqa: S607
stderr=subprocess.STDOUT,
shell=True, # noqa: S602
shell=True,
)
except subprocess.CalledProcessError as err:
out = (err.output or b"").decode("utf-8")
+3 -3
View File
@@ -32,7 +32,7 @@ class PathInfo(pathlib.PurePath, _BasePath):
def __new__(cls, *args):
if cls is PathInfo:
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo # noqa: PLW0642
return cls._from_parts(args) # type: ignore[attr-defined]
@@ -79,13 +79,13 @@ class PathInfo(pathlib.PurePath, _BasePath):
and self._cparts[:n] == other._cparts # type: ignore[attr-defined]
)
def relative_to(self, other):
def relative_to(self, other, *args, **kwargs):
# pathlib relative_to raises exception when one path is not a direct
# descendant of the other when os.path.relpath would return abspath.
# For DVC PathInfo we only need the relpath behavior.
# See: https://bugs.python.org/issue40358
try:
path = super().relative_to(other)
path = super().relative_to(other, *args, **kwargs)
except ValueError:
path = relpath(self, other)
return self.__class__(path)
+6
View File
@@ -18,3 +18,9 @@ def pytest_configure(config):
from .benchmarks.plugin import pytest_configure as bench_configure
bench_configure(config)
def pytest_report_header(config):
from .benchmarks.plugin import pytest_report_header as bench_report_header
return bench_report_header(config)
+3 -2
View File
@@ -3,7 +3,6 @@ import shutil
import pytest
from dvc.stage.cache import RunCacheNotSupported
from dvc.utils.fs import remove
from dvc_data.hashfile.tree import Tree
@@ -97,8 +96,10 @@ class TestRemote:
status_dir = dvc.cloud.status(dir_hashes)
_check_status(status_dir, ok=dir_hashes)
@pytest.mark.xfail(raises=RunCacheNotSupported, strict=False)
def test_stage_cache_push_pull(self, tmp_dir, dvc, remote):
if remote.scheme in ("http", "https"):
pytest.skip("HTTP remote does not support stage cache")
tmp_dir.gen("foo", "foo")
stage = dvc.stage.add(
deps=["foo"], outs=["bar"], name="copy-foo-bar", cmd="cp foo bar"
+1 -1
View File
@@ -71,7 +71,7 @@ class TmpDir(pathlib.Path):
def __new__(cls, *args, **kwargs):
if cls is TmpDir:
cls = WindowsTmpDir if os.name == "nt" else PosixTmpDir
cls = WindowsTmpDir if os.name == "nt" else PosixTmpDir # noqa: PLW0642
# init parameter and `_init` method has been removed in Python 3.10.
kw = {"init": False} if sys.version_info < (3, 10) else {}
+1 -1
View File
@@ -93,7 +93,7 @@ class Console:
styled=styled,
)
def write_json( # noqa: PLR0913
def write_json(
self,
data: Any,
indent: Optional[int] = None,
+1 -12
View File
@@ -5,7 +5,6 @@ import json
import os
import re
import sys
from collections.abc import Iterable, Iterator
from typing import TYPE_CHECKING, Optional
import colorama
@@ -13,8 +12,6 @@ import colorama
if TYPE_CHECKING:
from typing import TextIO
from dvc.fs import FileSystem
LARGE_DIR_SIZE = 100
TARGET_REGEX = re.compile(r"(?P<path>.*?)(:(?P<name>[^\\/:]*))??$")
@@ -246,7 +243,7 @@ def env2bool(var, undefined=False):
var = os.getenv(var, None)
if var is None:
return undefined
return bool(re.search("1|y|yes|true", var, flags=re.I))
return bool(re.search("1|y|yes|true", var, flags=re.IGNORECASE))
def resolve_output(inp: str, out: Optional[str], force=False) -> str:
@@ -410,14 +407,6 @@ def errored_revisions(rev_data: dict) -> list:
return result
def expand_paths(fs: "FileSystem", paths: Iterable[str]) -> Iterator[str]:
for path in paths:
if fs.isdir(path):
yield from fs.find(path)
else:
yield path
def isatty(stream: "Optional[TextIO]") -> bool:
if stream is None:
return False
+1 -1
View File
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
logger = logger.getChild(__name__)
STUDIO_URL = "https://studio.iterative.ai"
STUDIO_URL = "https://studio.dvc.ai"
def post(
+15 -16
View File
@@ -38,15 +38,15 @@ dependencies = [
"distro>=1.3",
"dpath<3,>=2.1.0",
"dulwich",
"dvc-data>=3.15,<3.16",
"dvc-data>=3.16.2,<3.17",
"dvc-http>=2.29.0",
"dvc-objects",
"dvc-render>=1.0.1,<2",
"dvc-studio-client>=0.20,<1",
"dvc-studio-client>=0.21,<1",
"dvc-task>=0.3.0,<1",
"flatten_dict<1,>=0.4.1",
"flufl.lock>=5,<8", # https://github.com/iterative/dvc/issues/9654
"fsspec",
"flufl.lock>=8.1.0,<9",
"fsspec>=2024.2.0",
"funcy>=1.14",
"grandalf<1,>=0.7",
"gto>=1.6.0,<2",
@@ -65,7 +65,7 @@ dependencies = [
"requests>=2.22",
"rich>=12",
"ruamel.yaml>=0.17.11",
"scmrepo>=3,<4",
"scmrepo>=3.3.4,<4",
"shortuuid>=0.5",
"shtab<2,>=1.3.4",
"tabulate>=0.8.7",
@@ -83,7 +83,7 @@ gdrive = ["dvc-gdrive>=3,<4"]
gs = ["dvc-gs>=3,<4"]
hdfs = ["dvc-hdfs>=3,<4"]
lint = [
"mypy==1.9.0",
"mypy==1.11.2",
"pandas-stubs",
"types-colorama",
"types-psutil",
@@ -95,13 +95,13 @@ lint = [
"typing-extensions",
]
oss = ["dvc-oss>=3,<4"]
s3 = ["dvc-s3>=3,<4"]
s3 = ["dvc-s3>=3.2.0,<4"]
ssh = ["dvc-ssh>=4,<5"]
ssh_gssapi = ["dvc-ssh[gssapi]>=4,<5"]
testing = [
"pytest-benchmark[histogram]",
"pytest-test-utils",
"virtualenv",
"uv",
]
tests = [
"dvc[testing]",
@@ -159,7 +159,7 @@ namespaces = false
write_to = "dvc/_dvc_version.py"
[tool.pytest.ini_options]
addopts = "-ra --cov-config pyproject.toml --dist worksteal --benchmark-skip"
addopts = "-ra --cov-config pyproject.toml"
filterwarnings = [
"error::ResourceWarning",
"error::pytest.PytestUnraisableExceptionWarning",
@@ -256,6 +256,7 @@ module = [
[tool.codespell]
ignore-words-list = "ba,datas,fo,uptodate,cachable,falsy"
skip = "CODE_OF_CONDUCT.md"
[tool.ruff]
output-format = "full"
@@ -263,21 +264,19 @@ show-fixes = true
[tool.ruff.lint]
ignore = [
"N818", "S101", "ISC001", "PT004", "PT007", "RET502", "RET503", "SIM105", "SIM108", "SIM117",
"TRY003", "TRY300", "PLR2004", "PLW2901", "LOG007",
"N818", "S101", "ISC001", "PT007", "RET502", "RET503", "SIM105", "SIM108", "SIM117",
"TRY003", "TRY300", "PERF203", "PLR2004", "PLW2901", "LOG007",
]
select = [
"F", "E", "W", "C90", "I", "N", "UP", "YTT", "ASYNC", "S", "BLE", "B", "A", "C4", "DTZ", "T10",
"EXE", "ISC", "ICN", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET",
"EXE", "ISC", "ICN", "LOG", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET",
"SLOT", "SIM", "TID", "TCH", "ARG", "PGH", "PLC", "PLE", "PLR", "PLW", "TRY",
"FLY", "PERF101", "LOG", "RUF", "RUF022", "RUF023", "RUF024", "RUF025", "RUF026", "RUF027"
"FLY", "PERF", "FURB", "RUF", "RUF022", "RUF023", "RUF024", "RUF026", "RUF027", "RUF029", "RUF030",
]
preview = true
explicit-preview-rules = true
[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
parametrize-names-type = "csv"
raises-extend-require-match-for = ["dvc.exceptions.DvcException", "dvc.scm.SCMError", "scmrepo.exceptions.SCMError"]
@@ -304,4 +303,4 @@ max-args = 10
"dvc/commands/**" = ["N806"]
"dvc/testing/**" = ["ARG002"]
"dvc/testing/benchmarks/**" = ["ARG001"]
"tests/**" = ["S", "ARG001", "ARG002", "TRY002", "TRY301"]
"tests/**" = ["S", "ARG001", "ARG002", "TRY002", "TRY301", "PERF"]
+1 -1
View File
@@ -3,4 +3,4 @@ if __name__ == "__main__":
import pytest
sys.exit(pytest.main(["-v", "-n=logical", *sys.argv[1:]]))
sys.exit(pytest.main(["-v", "-n=logical", "--dist=worksteal", *sys.argv[1:]]))

Some files were not shown because too many files have changed in this diff Show More