Compare commits

...
167 Commits
Author SHA1 Message Date
dependabot[bot] 4175f9ffa4 build: bump s3/gcsfs from 2021.10.1 to 2021.11.0
Bumps [gcsfs](https://github.com/fsspec/gcsfs) from 2021.10.1 to 2021.11.0.
- [Release notes](https://github.com/fsspec/gcsfs/releases)
Bumps [s3fs](https://github.com/fsspec/s3fs) from 2021.10.1 to 2021.11.0.
- [Release notes](https://github.com/fsspec/s3fs/releases)
- [Changelog](https://github.com/fsspec/s3fs/blob/main/release-procedure.md)

---
updated-dependencies:
- dependency-name: s3fs
  dependency-type: direct:development
  update-type: version-update:semver-minor
- dependency-name: gcsfs
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2021-11-08 11:13:52 +08:00
Guðmundur HeimissonandRuslan Kuprieiev c5b1a73dc5 webhdfs: expose kerberos and https options (#6936)
* Add fsspec options for webhdfs

* Add requests-kerberos dependency

* Update dvc/fs/webhdfs.py

Simplify config.pop.

Co-authored-by: Ruslan Kuprieiev <[email protected]>

* Update dvc/fs/webhdfs.py

* Simplify kerb_kwargs config setting.

Co-authored-by: Ruslan Kuprieiev <[email protected]>

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-11-07 20:42:10 +02:00
Saugat Pachhai (सौगात) 044e79000d deps: bump rich to >=10.13.0
It comes with the support for no. of parameters that matches with json.dumps.
See: https://github.com/willmcgugan/rich/pull/1547 and
https://github.com/willmcgugan/rich/pull/1644.

Also added support for printing json to stderr. Before we used to use json.dumps
and rich.json.JSON based on isatty. Now we use rich.print_json on both
tty and non-tty conditions, which may avoid different behaviours in the
future.
2021-11-07 19:38:06 +02:00
karajan1001 be6128470f machine status: include two new field in the output
1. include instance_type and instance_gpu in the output.
2. rerange the field by importance.
2021-11-04 15:10:45 +02:00
Saugat Pachhai (सौगात) dd0d4f5a2c exp init: don't try tracking params if they are skipped 2021-11-03 19:21:10 +05:45
Saugat Pachhai (सौगात) 6e0dfb94e4 exp init: hide message about workspace tree on --explicit 2021-11-03 18:59:25 +05:45
Saugat Pachhai (सौगात) ce6c8a5678 exp init: fix --explicit failing with KeyError
Fixes #6917
2021-11-03 16:42:45 +05:45
dependabot[bot] b927337a30 build(deps): bump actions/checkout from 2.3.5 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.5...v2.4.0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2021-11-03 12:47:22 +02:00
Saugat Pachhai (सौगात) 03b372a073 exp init: ask for command before paths 2021-11-03 05:23:40 +05:45
Saugat Pachhai (सौगात) 915a755e2f build: remove numpy restriction 2021-11-03 05:23:17 +05:45
Paweł Redzyński 22cc2a5cbc plots: fix resolve template path bug
Fix: #6854
2021-11-02 16:49:56 +02:00
Saugat Pachhai (सौगात) a67d8a44b3 exp init: make confirmation prompt at the end similar to others 2021-11-02 17:24:09 +05:45
Saugat Pachhai (सौगात) 80a52e65ad exp init: minor refactor on way it returns prompted values 2021-11-02 14:57:40 +05:45
Saugat Pachhai (सौगात) 208ccb2430 exp init: autostage .gitignores, dvc.yaml and params file 2021-11-02 12:00:06 +05:45
dependabot[bot] 97e97c7f24 build(deps-dev): bump filelock from 3.3.1 to 3.3.2
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.3.1 to 3.3.2.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.3.1...3.3.2)

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

Signed-off-by: dependabot[bot] <[email protected]>
2021-11-02 08:16:37 +05:45
dependabot[bot] 1d1451ddd3 build(deps): bump pyinstaller from 4.5.1 to 4.6
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 4.5.1 to 4.6.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/v4.6/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v4.5.1...v4.6)

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

Signed-off-by: dependabot[bot] <[email protected]>
2021-11-02 08:16:18 +05:45
pre-commit-ci[bot] 0cc7c4c02b [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/ambv/black: 21.9b0 → 21.10b0](https://github.com/ambv/black/compare/21.9b0...21.10b0)
2021-11-02 06:30:13 +05:45
Saugat Pachhai d3d459caa0 Revert "deps: require chardet as a dependency"
This reverts commit a4dc8ddc74.
2021-11-02 06:17:31 +05:45
Saugat Pachhai a4dc8ddc74 deps: require chardet as a dependency
Looks like azure-core is dependent on a dependency `chardet` that aiohttp brought it with, which has been replaced with `charset_normalizer`.
But azure-core depends on internal of aiohttp and looks like it tries to set it's own encoding parser, `chardet` which is now not available.
2021-11-01 18:25:39 +02:00
Saugat Pachhai 3eceeea3e8 Remove python 3.10 restriction for pyarrow
Pyarrow 6 is now available for Python 3.10. Fixes https://github.com/iterative/dvc/issues/6878.
2021-11-01 20:00:21 +05:45
Saugat Pachhai (सौगात) 8fc959060a tests: skip based on conditional import on pyarrow
From now on, pyarrow dependency is optional for the tests.
2021-11-01 19:15:28 +05:45
David de la Iglesia Castro 0638d99fe3 LiveTask: write output path at start/end
Writing at the end of each step (create_summary) can mess up with some ML Framework progress bars, I moved the message to the monitor task, as the path won't change during training.

Fixes iterative/dvclive#177
2021-10-29 14:41:03 +02:00
David de la Iglesia Castro 24abc6f328 TabularData: Add dropna method.
This method mimics pandas `dropna`.
2021-10-29 15:28:36 +03:00
David de la Iglesia Castro 4dd544121f plots: Use URI for --open.
We were previously using relpath in *all* cases, mainly to support WSL but relpath doesn't work
on macOS.

Added a check for using relpath *only* for WSL.

Fixes #6092
2021-10-29 13:27:12 +02:00
Ruslan Kuprieiev 869ea442ef output: save: remove redundant changed() call
No longer needed since object introduction, as we will compare staged
object during commit/checkout anyway.
2021-10-29 01:45:10 +03:00
Ruslan Kuprieiev 922eb20e44 staging: don't use move 2021-10-27 20:19:17 +03:00
Ruslan Kuprieiev a2f91ed5fd checkout: always confirm link type
Currently whether or not link confirmation will run depends on
particular filesystem and existing workspace link type, which introduces
inconsistency in behaviour, where we sometimes catch unsupported links
before touching user data and sometimes we don't.
2021-10-27 20:19:17 +03:00
Ruslan Kuprieiev 017762fc2c checkout: don't forget to protect() back objects
Depending on a filesyste (e.g. on NTFS), `_remove` might reset
read-only permissions in order to delete a hardlink to protected object,
which will also reset it for the object itself, making it unprotected,
so we need to protect it back.
2021-10-27 20:19:17 +03:00
Ruslan Kuprieiev 874aceccf8 system: use linkat instead of link
`os.link` is broken across platforms when dealing with symlinks.

See https://bugs.python.org/issue41355 for more info.
2021-10-27 20:19:17 +03:00
Ruslan Kuprieiev 41aee9a3b7 localfs: use mtime/size/inode for checksum
Currently it depends on the whole `info()` output, which has too much
information that doesn't really matter. To check if file contents didn't
change mtime, size, inode combo is enough.
2021-10-27 20:19:17 +03:00
Peter Rowlands (변기호) 0d895e2661 exp: add initial ssh executor (#6833)
* executor: move local specific git initialization out of base

* machine: return kwargs for executor init instead of executor object

* executor: add initial ssh executor

* ssh-executor: disable strict host checking for dvc-machine instances

* initial tests for ssh executor

* drop GIT_SSH_COMMAND usage

* push: accept destination odb as a parameter

* separate cache initialization into executor.init_cache

* add test for ssh executor cache init

* remove dead code

* machine: fixture-ize default test scenarios

* test SSHExecutor.from_machine

* tpi: return default SSH port for constructing executors
2021-10-26 16:08:17 +09:00
Saugat Pachhai 14eaba4cdb exp init: iterate on ui, tree and show to-be-generated yaml content (#6750)
* iterate on ui, has tree and shows to-be-generated yaml content

* order fs tree structure alphabetically

* always show the heading

* hide dvclive on tree if not on live mode

* Change message at the end

* s/skip/omit

* add link on the header

* remove newline on non-interactive mode
2021-10-26 08:22:33 +05:45
Dave Berenbaum 3064738a34 fix msg in for ExperimentExistsError (#6864) 2021-10-26 10:55:08 +09:00
GaoandPeter Rowlands c07592131a exp run: add experiment name check. (#6848)
* Add experiment name check.

1. Add experiment name check (https://git-scm.com/docs/git-check-ref-format)
2. Add duplicate exp name check.
3. Add some unit test for it.

* Ban slash / in dvc exp names

* Use dulwich backend for ref name checking

* Some bug fix

* Update dvc/repo/experiments/__init__.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Update dvc/repo/experiments/__init__.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

* Some review changes

* Make some funtion more reusable.

Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-10-25 20:16:37 +09:00
Saugat Pachhai 52a9cddc14 exp init: remove default in cli, add default in help instead (#6862)
The default made it harder to detect between the default and explicitly provided values, which we need for interactive mode.
2021-10-25 16:32:17 +05:45
Ruslan Kuprieiev 38cbebfb28 fs: transfer: try reflink (#6853)
If supported, reflink, will provide the fastest transfer possible.
That being said, right now we use `move` in most of the places,
which still takes presidence.

Pre-requisite for #6387
2021-10-24 22:46:02 +03:00
dependabot[bot]andSaugat Pachhai 5017c2f282 build(deps): update pyparsing requirement from ==2.4.7 to >=2.4.7
build(deps): update pyparsing requirement from ==2.4.7 to >=2.4.7 

Updates the requirements on [pyparsing](https://github.com/pyparsing/pyparsing) to permit the latest version.
- [Release notes](https://github.com/pyparsing/pyparsing/releases)
- [Changelog](https://github.com/pyparsing/pyparsing/blob/master/CHANGES)
- [Commits](https://github.com/pyparsing/pyparsing/compare/pyparsing_2.4.7...pyparsing_3.0.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai <[email protected]>
2021-10-24 15:22:46 +00:00
dependabot[bot] 1bf6cc7c2f build(deps): bump pylint-pytest from 1.0.3 to 1.1.2 (#5894)
Bumps [pylint-pytest](https://github.com/reverbc/pylint-pytest) from 1.0.3 to 1.1.2.
- [Release notes](https://github.com/reverbc/pylint-pytest/releases)
- [Changelog](https://github.com/reverbc/pylint-pytest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reverbc/pylint-pytest/compare/pylint-pytest-1.0.3...pylint-pytest-1.1.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-24 20:08:58 +05:45
dependabot[bot] 27666ffba7 build(deps-dev): Bump dvclive from 0.4.1 to 0.4.2 (#6846)
Bumps [dvclive](https://github.com/iterative/dvclive) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/iterative/dvclive/releases)
- [Commits](https://github.com/iterative/dvclive/compare/0.4.1...0.4.2)

---
updated-dependencies:
- dependency-name: dvclive
  dependency-type: direct:development
  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>
2021-10-24 19:03:24 +05:45
Ruslan Kuprieiev e2d705fd76 link: use proper exceptions (#6852)
* link: use proper exceptions

* system: use ENOTSUP instead of NotImplementedErrorwq
2021-10-23 02:38:17 +03:00
Saugat Pachhai 719e93192f move scm.install to dvc.install (#6849)
scm.install does 3 different things, and all of those are
very much tied to DVC. So, as we are splitting scm out,
it should now belong to `dvc.install`.
2021-10-22 14:53:33 +03:00
Saugat Pachhai 501256708d use system hook for mypy; fix raised issues (#6851) 2021-10-22 15:40:54 +05:45
Saugat Pachhai 800fbc7ad0 Use -n=auto in tests
Linux/Windows machines have only 2 cores, whereas macOS machines have 3 cores. Maybe we are stretching available resources by 2x, let's see if `-n=auto` improves test freezes.
2021-10-22 08:46:55 +05:45
Peter Rowlands (변기호) 73f2fa8c93 git: use asyncssh as dulwich SSH vendor (#6797)
* git: support client kwargs for auth in remote methods

* tests: use docker for git/ssh server

* move dulwich backend into subdir

* add base sync<->async support

* dulwich: add initial asyncssh SSH vendor

* dulwich: use asyncssh as the default SSH vendor

* add test for git over ssh

* test read (fetch) over ssh

* ci: re-enable ssh tests

temporary workaround for #6788, #6789

* move async stubs into dvc.scm.asyn

* tests: use sshfs and not dvc.fs.ssh

* allow explicit 0-length read in asyncssh vendor
2021-10-21 14:36:45 +09:00
b652670cbf Dependabot/pip/pylint 2.11.1 (#6843)
* build(deps): Bump pylint from 2.9.6 to 2.11.1

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.9.6 to 2.11.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.9.6...v2.11.1)

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

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

* fix pylint problems

* Updation after rebase.

* system: use binary mode

* Set remote execution script encoding type locale

* Update dvc/machine/__init__.py

Co-authored-by: Peter Rowlands (변기호) <[email protected]>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
Co-authored-by: Peter Rowlands (변기호) <[email protected]>
2021-10-20 14:34:10 +03:00
Gao 6de3e734ed Revert "build(deps): Bump pylint from 2.9.6 to 2.11.1 (#6636)" (#6842)
This reverts commit 464620806c.
2021-10-20 12:41:00 +03:00
Gao 4b2bec89ec dvc machine: add a new command rename to it (#6633)
* `dvc machine`: add a new command `rename` to it

fix #6481
1. add new command `dvc machine rename`
2. add a unit test for the call.
3. add several functional tests for it.t push myself fix6481

* Move rename logic to tpi

* use tpi.state_mv instead of rename

* Rebase to a newer version

* Update tpi version

* Remove unused assert and try catch
2021-10-20 15:57:44 +09:00
Saugat Pachhai 6c0bc9563c enable pywin32 for Python3.10, make it an optional dependency (#6841) 2021-10-20 11:25:00 +05:45
f964a44e7c Exp show md (#6799)
* Added `--md`/`--show-md` option to `exp show`.

* Added test_show_experiments_md

* Fixed re-defining all_experiments

* Fixed all_experiments

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

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

* Added dest markdown

* Added textwrap.dedent

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

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

* Update experiments.py

* Apply suggestions from code review

Co-authored-by: Saugat Pachhai <[email protected]>

* Update tests/unit/command/test_compat_flag.py

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai <[email protected]>
2021-10-20 11:04:57 +05:45
464620806c build(deps): Bump pylint from 2.9.6 to 2.11.1 (#6636)
* build(deps): Bump pylint from 2.9.6 to 2.11.1

Bumps [pylint](https://github.com/PyCQA/pylint) from 2.9.6 to 2.11.1.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.9.6...v2.11.1)

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

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

* fix pylint problems
* system: use binary mode

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: karajan1001 <[email protected]>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-10-20 10:32:44 +08:00
Saugat Pachhai 4244e5a475 fix #6835 (#6837) 2021-10-19 21:59:09 +03:00
Saugat Pachhai 0c41a25bbe move some configs to pyproject.toml (#6834) 2021-10-19 19:47:51 +03:00
Matthew Beckers 26f435a433 exp run: throw error if new parameters are added by --set-param (#6521)
* throw error if new parameters are added by dvc run exp -S

Throws an error if dvc run exp --set-param references a parameter key path
that is not in the target config file. This is done to prevent silent errors
in experimentation when new params are accidentally added instead of modifying
current ones due to typos.

This implementation is based on initial suggestions in issue #5477.

* fix mypy confusion from overwriting a var

* tests: remove modify param test that was adding params

One of the test in test_modify_params was still adding a parameter

* tests: remove param modification during exp run in test_untracked

Spurious usage of modifying a parameter when the params.yaml is not yet existing in test_untracked.

* remove benedict dependency for verifying params

Instead, the keypath util module from benedict is adapted into the dvc codebased and used
natively to verify that a keypath exists within a dict.

* exp: list all invalid param modifications in error message

* exp-run: more specific exception for new params via --set-param

Uses established MissingPramsError instead of DvcException. Modified message to be more similar to other uses of this exception.

* Revert "remove benedict dependency for verifying params"

This reverts commit 6170f16dcc4033667fbacb0d0bc13c60898f58c4.

* exp: use internal benedict import for validating params

* exp: combine new param validation into merge_params

Moves new param validation into existing merge_params function as an optional check

* exp: minor ammendments to error message

* Revert "tests: remove param modification during exp run in test_untracked"

This reverts commit c9eb1a62af58586509ded362cc79e75ddc4b119c.

* exp: fix test_untracked to not fail due to new params

Fixed by creating the params file with the new parameter already present.
2021-10-19 17:46:36 +09:00
966dc8a1ca machine: Add a new command dvc machine status (#6649)
* machine: Add a new command `dvc machine status`

fix#6482
1. add a new command `dvc machien status`
2. add a unit test for the cli call.
3. add a unit test for the shown result.

* Update dvc/command/machine.py

Co-authored-by: Saugat Pachhai <[email protected]>

* Update dvc/command/machine.py

Co-authored-by: Saugat Pachhai <[email protected]>

* Update machine.py

* Remove sensitive fields

* Update dvc/machine/__init__.py

Co-authored-by: Casper da Costa-Luis <[email protected]>

* Update tests/func/machine/__init__.py

Co-authored-by: Casper da Costa-Luis <[email protected]>

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

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

* Use white list to limit shown fields

Co-authored-by: Saugat Pachhai <[email protected]>
Co-authored-by: Casper da Costa-Luis <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-10-19 17:38:11 +09:00
eggqq007andZhenyu Gao 6d96ecb376 dvcfs: handle new workspace subdirectories in walk (#6811)
1. add try-catch to catch KeyError exception while walking dvc folder
2. Fix #6695

Co-authored-by: Zhenyu Gao <[email protected]>
2021-10-19 17:33:02 +09:00
Batuhan Taskaya 9e57d1c32d gdrive: remove redundant _gdrive_retry function (#6831) 2021-10-18 21:52:39 +00:00
Batuhan Taskaya c08655ac38 plots: fix the dictionary to return PathInfos (#6830) 2021-10-18 22:04:21 +02:00
David de la Iglesia Castro 4f16852005 exp/metrics/params diff: Replaced Old/New with a_rev/b_rev (#6814)
* Replaced Old/New with a_rev/b_rev

* Fixed tests
2021-10-18 21:09:02 +03:00
Ruslan Kuprieiev 7bef6fd64f setup: bump fsspec deps (#6828) 2021-10-18 21:01:03 +03:00
pre-commit-ci[bot] 422d883e18 [pre-commit.ci] pre-commit autoupdate (#6829)
updates:
- [github.com/lovesegfault/beautysh: v6.1.0 → v6.2.1](https://github.com/lovesegfault/beautysh/compare/v6.1.0...v6.2.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-10-18 20:47:48 +03:00
Saugat Pachhai 48004b1340 Revert "webdav: limit httpx to fix the CI" (#6827)
* Revert "webdav: limit httpx to fix the CI (#6800)"

This reverts commit af34f5a0f1.

* Bump webdav4 requirement
2021-10-18 20:32:46 +03:00
dependabot[bot] 2ae421cea2 build(deps-dev): Bump filelock from 3.3.0 to 3.3.1 (#6825)
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.3.0...3.3.1)

---
updated-dependencies:
- dependency-name: filelock
  dependency-type: direct:development
  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>
2021-10-18 19:53:37 +05:45
dependabot[bot] 812fde9891 build(deps-dev): Bump types-requests from 2.25.9 to 2.25.11 (#6820)
Bumps [types-requests](https://github.com/python/typeshed) from 2.25.9 to 2.25.11.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-type: direct:development
  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>
2021-10-18 19:51:13 +05:45
dependabot[bot] 4ed4766692 build(deps): Bump actions/checkout from 2.3.4 to 2.3.5 (#6817)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.4...v2.3.5)

---
updated-dependencies:
- dependency-name: actions/checkout
  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>
2021-10-18 19:43:41 +05:45
dependabot[bot] 9943cd43b1 build(deps-dev): Bump types-tabulate from 0.8.2 to 0.8.3 (#6790)
Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.2 to 0.8.3.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-tabulate
  dependency-type: direct:development
  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>
2021-10-18 19:42:50 +05:45
dependabot[bot] 7d3f35e6ab build(deps-dev): Bump types-toml from 0.10.0 to 0.10.1 (#6821)
Bumps [types-toml](https://github.com/python/typeshed) from 0.10.0 to 0.10.1.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-toml
  dependency-type: direct:development
  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>
2021-10-18 20:57:43 +09:00
dependabot[bot] 4de261f717 build(deps-dev): Bump dvclive from 0.4.0 to 0.4.1 (#6786)
Bumps [dvclive](https://github.com/iterative/dvclive) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/iterative/dvclive/releases)
- [Commits](https://github.com/iterative/dvclive/compare/0.4.0...0.4.1)

---
updated-dependencies:
- dependency-name: dvclive
  dependency-type: direct:development
  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>
2021-10-18 13:33:26 +03:00
Paweł Redzyński db75878bef plots: make generator out of collect (#6793) 2021-10-15 17:24:02 +03:00
Ruslan Kuprieiev 7f5c53c10e ui: write_json: don't forget json kwargs (#6810)
Regression from #6743

Fixes #6803
2021-10-15 01:30:22 +03:00
Ruslan Kuprieiev 259d590527 setup.cfg: remove types-paramiko (#6807)
We are no longer using them since migration to sshfs.

Closes #6806
2021-10-14 16:00:10 +03:00
dependabot[bot]andRuslan Kuprieiev 709bfe598f build(deps): Update flufl-lock requirement from <4,>=3.2 to >=5 (#6576)
* build(deps): Update flufl-lock requirement from <4,>=3.2 to >=3.2,<7

Updates the requirements on [flufl-lock](https://gitlab.com/warsaw/flufl.lock) to permit the latest version.
- [Release notes](https://gitlab.com/warsaw/flufl.lock/tags)
- [Commits](https://gitlab.com/warsaw/flufl.lock/compare/3.2...6.0)

---
updated-dependencies:
- dependency-name: flufl-lock
  dependency-type: direct:production
...

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

* upgrade flufl.lock

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-10-14 15:26:40 +03:00
David de la Iglesia Castro 7e4f443002 Added test_show_experiments_sort_by (#6798)
* Added test_show_experiments_sort_by

* Added comment

* help formatting
2021-10-14 14:49:00 +03:00
2a3a93314b pathspec 0.9.0 quickfix (#6689)
* pathspec 0.9.0 quickfix

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

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

* restrict requirement properly

Co-authored-by: Gao <[email protected]>

* revert change_rule update

* adjust tests to pathspec 0.9.0

* lower bound restrict pathspec

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Gao <[email protected]>
Co-authored-by: Saugat Pachhai (सौगात) <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2021-10-14 14:25:03 +03:00
Paweł Redzyński a1b146669c plots: load templates from package (#6550)
* plots: vega: make interactive linear default plot

* plots: stop dumping default templates

* plots: move templates to package initial

* plots: update default plots schema

* plots: write defaults on init

* templates: migrate to importlib.resources

* fixup

* fixup
2021-10-14 08:40:31 +02:00
Peter Rowlands (변기호) b8478a8b04 machine: re-add instance dir management from TPI (#6748)
* machine: re-add instance dir management from TPI

* update default constructor

* machine: lazy load Repo.machine
2021-10-14 12:40:30 +09:00
Michael Lingelbach 0ae9130052 dvc: add 'repos' list to pre-commit config if it does not exist (#6795) 2021-10-13 22:50:25 +03:00
Batuhan Taskaya af34f5a0f1 webdav: limit httpx to fix the CI (#6800) 2021-10-13 16:49:54 +03:00
Ruslan Kuprieiev e9a7e0657d dvc: drop 3.6 support (#6792)
* dvc: drop 3.6 support

Considering that 3.6 has ~2 months until EOL, and our deps are
dropping support for it already (e.g. pygit2), we are droppipping
it as well. This will reduce the load on our test CI as well, since
we've started supporting 3.10 recently.

* setup.cfg: adjust requirements

* daemon: remove 3.6 logic

* tests: utils: remove pre 3.7 logic
2021-10-12 18:53:28 +03:00
Saugat PachhaiandRuslan Kuprieiev 8abda93e3e use setuptools_scm and build_requires (#6775)
* use setuptools_scm and build_requires

* remove kwargs from setup.py

* fix tests; fetch in depth in github actions

* use build to build sdist/wheel

* try updating pip and setuptools

* update pip

* remove or unpin wheel

* bring back the version in pyinstaller

* Update scripts/build.py

Co-authored-by: Ruslan Kuprieiev <[email protected]>
2021-10-12 17:58:14 +03:00
Ruslan Kuprieiev 53255c22b3 gha: tests: temporarily disable some remote tests (#6789)
* gha: tests: temporarily disable some remote tests

See #6788

* machine: upgrade tpi version

```
dvc/machine/backend/terraform.py:28:19: E1101: Instance of 'TerraformBackend' has no '_default_resource' member; maybe 'default_resource'? (no-member)
```

* tests: remotes: require --s3 for s3 tests
2021-10-12 17:26:47 +03:00
Paweł RedzyńskiandJorge Orpinel 6da15629c4 plots: modify: add note on modyfying image type plots (#6742)
* plots: modify: add note on modyfying image type plots

* Update dvc/command/plots.py

Co-authored-by: Jorge Orpinel <[email protected]>

Co-authored-by: Jorge Orpinel <[email protected]>
2021-10-12 16:08:49 +03:00
Ruslan Kuprieiev 5cfe61928e tree: use oids instead of objects (#6785) 2021-10-12 05:39:10 +03:00
Ruslan Kuprieiev 32467ada32 objects: extract size/nfiles to Meta (#6784)
* stage: capture metadata

* tree: capture metadata

* objects: extract size/nfiles from HashInfo/HashFile/Tree to Meta
2021-10-12 03:06:18 +03:00
pre-commit-ci[bot] 5cbaa8d98e [pre-commit.ci] pre-commit autoupdate (#6782)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.910 → v0.910-1](https://github.com/pre-commit/mirrors-mypy/compare/v0.910...v0.910-1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-10-12 02:27:56 +03:00
Ruslan Kuprieiev 4762c38ff7 dvc: bump to 2.8.1 2021-10-11 16:09:51 +03:00
Ruslan Kuprieiev 66acbc892a setup.cfg: use text/x-rst instead of text/rst
Per https://packaging.python.org/specifications/core-metadata/#description-content-type
2021-10-11 16:09:29 +03:00
Ruslan Kuprieiev d2edbae7e3 dvc: bump to 2.8.0 2021-10-11 15:47:58 +03:00
Batuhan Taskaya be0c827b95 fs: add basic type annotations to FsspecWrapper (#6779) 2021-10-11 15:15:56 +03:00
Paweł Redzyński d6ecef7169 plots: fix wrong link (#6693) 2021-10-11 12:22:20 +02:00
eggqq007andZhenyu Gao e8b649a75f Fix exp show --sort-by issue #6675 (#6764)
Co-authored-by: Zhenyu Gao <[email protected]>
2021-10-11 16:46:38 +09:00
Batuhan Taskaya 8767f42f39 gs: use 3-char prefixes (#6759)
Resolves #6691. Normally it should have inherited this from the `ObjectFSWrapper`, but this seems like a leftover.
2021-10-11 16:16:17 +09:00
Paweł Redzyński 8580e66d60 gha: move cancelaltion into other workflows (#6771) 2021-10-08 18:03:07 +03:00
Saugat Pachhai c24ae2b0c3 exp init: show defaults in help message (#6763) 2021-10-08 20:14:12 +05:45
Saugat Pachhai 4e17279440 exp init: add config support (#6761) 2021-10-08 20:14:01 +05:45
Batuhan Taskaya 50cdf4f213 fsspec: normalize checksum() to return string (#6767) 2021-10-08 16:57:37 +03:00
Saugat Pachhai f628f670c9 advertise Python 3.10 support (#6772)
* advertise Python 3.10 support

Pygit2 does not have windows wheels yet and no pyarrow
for 3.10. But these platform-dependent things and
remote-dependent things should not stop us from
advertising support as we are pure-python based
package and we cannot claim support for all of our
dependencies.

* set pygit2 for Python 3.6 to <1.7
2021-10-08 15:50:05 +03:00
Batuhan Taskaya d7489bb65f hdfs: enable mocked hdfs tests by default (#6766) 2021-10-08 12:42:41 +02:00
dependabot[bot]andPaweł Redzyński 5a4f4d3a8e build(deps): Bump filelock from 3.2.0 to 3.3.0 (#6737)
* build(deps): Bump filelock from 3.2.0 to 3.3.0

Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.2.0...3.3.0)

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

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

* tests: remotes: ignore abstract class instantiation for filelock

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Paweł Redzyński <[email protected]>
2021-10-08 15:36:33 +05:45
dependabot[bot] 2c24b24562 build(deps-dev): Bump types-requests from 2.25.6 to 2.25.9 (#6770)
Bumps [types-requests](https://github.com/python/typeshed) from 2.25.6 to 2.25.9.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-type: direct:development
  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>
2021-10-08 14:59:09 +05:45
dependabot[bot] df9f76c2c0 build(deps-dev): Bump types-paramiko from 0.1.9 to 2.7.1 (#6769)
Bumps [types-paramiko](https://github.com/python/typeshed) from 0.1.9 to 2.7.1.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-paramiko
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-08 14:58:19 +05:45
dependabot[bot] 0db3ece060 build(deps-dev): Bump types-toml from 0.1.5 to 0.10.0 (#6768)
Bumps [types-toml](https://github.com/python/typeshed) from 0.1.5 to 0.10.0.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-toml
  dependency-type: direct:development
  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>
2021-10-08 14:58:08 +05:45
Saugat Pachhai 7c437cc349 make dependabot updates work (#6760)
Dependabot was throwing Illformed requirements `"< '3.10'"` for the
hdfs. But I noticed that it did accept other requirements where we
do similar. So it seems the indentation fixes it.

Even then, dependabot did not update any dependencies. I removed
setup.py file, after which immediately it started creating PRs.
So, I recknoed that it must have been due to some mechanism
in place from dependabot to disallow execution, so I tried to
remove few blocks of code from setup.py. And, I was eventually
able to figure out that it was from the block where we import version.

So, I added a try-except block and it magically again worked. :)

Let's see how it goes.
2021-10-08 14:46:47 +05:45
Saugat Pachhai 3ed5efceb7 move defaults to the class level (#6762) 2021-10-08 12:18:28 +05:45
dependabot[bot]andPaweł Redzyński d33df60e41 build(deps): Bump dvclive from 0.3.0 to 0.4.0 (#6753)
* build(deps): Bump dvclive from 0.3.0 to 0.4.0

Bumps [dvclive](https://github.com/iterative/dvclive) from 0.3.0 to 0.4.0.
- [Release notes](https://github.com/iterative/dvclive/releases)
- [Commits](https://github.com/iterative/dvclive/compare/0.3.0...0.4.0)

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

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

* live: adjust tests to dvclive==0.4.0

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Paweł Redzyński <[email protected]>
2021-10-08 07:02:54 +05:45
Saugat Pachhai c5415342d0 move requirements to setup.cfg (#6758)
* move requirements to setup.cfg

* hash based on setup.cfg

* do not break webhdfs installation
2021-10-08 07:00:14 +05:45
Paweł Redzyński dd86076ba6 gha: cancel previous workflows (#6756) 2021-10-07 17:59:44 +03:00
Ruslan Kuprieiev 7dc3859400 pyinstaller: hooks: remove webhdfs 2021-10-07 17:30:22 +03:00
Batuhan Taskaya 4f3b28e268 webhdfs: migrate to fsspec (#6662)
* webhdfs: migrate to fsspec

* skip the tests that can't use hdfs
2021-10-07 17:02:31 +03:00
Ruslan Kuprieiev 32f435c47f gha: benchmarks: fix pytest flags (#6749)
Kudos @skshetry for catching.
2021-10-06 17:07:41 +03:00
Batuhan Taskaya 054feae778 hdfs: migrate to fsspec (#6604)
* hdfs: migrate to fsspec

* normalize windows paths when delegating back to fsspec
2021-10-06 15:44:27 +03:00
Saugat Pachhai 2abe515391 Pin dvclive to 0.3.0 2021-10-06 14:28:36 +05:45
eggqq007andZhenyu Gao 1eb89a2dc0 command: Should not consume 'print' to output default remote info (#6650)
1. should not consume 'print' to output default remote info,
   otherwise the --quiet would not work.

Co-authored-by: Zhenyu Gao <[email protected]>
2021-10-06 09:27:47 +05:45
eggqq007andZhenyu Gao 9c300a0a1c command: output prettify json in tty (#6743)
1. update diff.py,experiments.py,metrics.py,params.py,status.py to
print prettify json
2. rewrite write_json in __init__.py to accept default function
3. add new unit test for write_json in tests/unit/ui/test_console.py
4. Fix #6712

Co-authored-by: Zhenyu Gao <[email protected]>
2021-10-05 20:19:56 +05:45
Ruslan Kuprieiev 7b68a6f0ca gha: run benchmarks on PRs (#6733) 2021-10-05 14:18:43 +03:00
Saugat Pachhai ee823a59b9 Run on Python3.10 (#6745) 2021-10-05 16:46:53 +05:45
David de la Iglesia CastroandSaugat Pachhai 16f404b16a Fix dvclive running subdir (#6740)
* Fix dvclive running subdir

* Apply suggestions from code review

Co-authored-by: Saugat Pachhai <[email protected]>

* Applied suggestions

* Update tests/func/test_live.py

Co-authored-by: Saugat Pachhai <[email protected]>
2021-10-05 13:16:33 +05:45
Saugat Pachhai 0eda4bd91c exp init: only ask for that are not provided in an interactive mode (#6739) 2021-10-04 14:33:08 +05:45
Saugat Pachhai a098a50c8d move exp init logic to dvc.repo.experiments.init (#6738) 2021-10-04 12:26:34 +05:45
dependabot[bot] f57a378c0c build(deps): Bump jaraco-windows from 5.6.0 to 5.7.0 (#6735)
Bumps [jaraco-windows](https://github.com/jaraco/jaraco.windows) from 5.6.0 to 5.7.0.
- [Release notes](https://github.com/jaraco/jaraco.windows/releases)
- [Changelog](https://github.com/jaraco/jaraco.windows/blob/main/CHANGES.rst)
- [Commits](https://github.com/jaraco/jaraco.windows/compare/v5.6.0...v5.7.0)

---
updated-dependencies:
- dependency-name: jaraco-windows
  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>
2021-10-04 10:12:47 +05:45
dependabot[bot] 3e0a50e52e build(deps): Bump pytest-cov from 2.12.1 to 3.0.0 (#6736)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.1 to 3.0.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.12.1...v3.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-04 09:31:27 +05:45
Guillaume COTER ec72866eb9 Bump typing_extensions to >=3.7.4 (#6731) (#6732) 2021-10-03 20:10:28 +05:45
Batuhan Taskaya e53d390ff9 http: explicitly ignore all http-related errors (from server side to client connection drops) (#6728) 2021-10-02 20:42:23 +03:00
Batuhan Taskaya dd330ac592 setup: bump fsspec/s3fs/gcsfs (#6727) 2021-10-02 18:54:09 +02:00
pwoolvett 4ccb712b6d build(setup): avoid leaking tests package into site-packages (#6585)
Signed-off-by: Pablo Woolvett <[email protected]>
2021-10-02 16:46:45 +03:00
Saugat Pachhai ecee2d3522 Move static metadata to setup.cfg (#6581) 2021-10-02 16:08:30 +03:00
20bb6ddf3c doctor: fixes unknown cache directory error on some windows mapped network shares (#6364)
* doctor: fixes unknown cache director error on some windows mapped network shares

On Windows, resolve calls os.path._getfinalpathname, which calls GetFinalPathNameByHandle.
This starts from the final resolved path of the file in the NT namespace (e.g. \Device\Mup\server\share...)
and works backward as required to get the VOLUME_NAME_DOS. If the final path is a UNC share (i.e. \Device\Mup),
We fix this by calling resolve() also on the partition mountpoints
We also take the opportunity to make checks in case a PermissionError is returned to us

Fixes github.com/iterative/dvc/issues/6305

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

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

* doctor: fixes unknown cache director error on some windows mapped network shares

On Windows, resolve calls os.path._getfinalpathname, which calls GetFinalPathNameByHandle.
This starts from the final resolved path of the file in the NT namespace (e.g. \Device\Mup\server\share...)
and works backward as required to get the VOLUME_NAME_DOS. If the final path is a UNC share (i.e. \Device\Mup),
We fix this by calling resolve() also on the partition mountpoints
We also take the opportunity to make checks in case a PermissionError is returned to us

Fixes #6305

Co-authored-by: CHETOUAN <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: CHETOUAN <[email protected]>
2021-10-02 16:06:42 +03:00
Saugat Pachhai e58fcd864b [WIP] exp init: introduce --with={default,live} flag (#6716)
* exp init: introduce `--with={default,live}` flag

This flag will select the different mode of outputs of stage.
With `--with=live`, it'll enable `live` setting in the stage.
On interactive mode, it'll always ask user if they want to
use live (unless `--with=live` is specified), and then it'll
skip metrics/plots question and moves on to ask for path to
dvclive path.

Also, if live setting is enabled in the stage, it'll set the
`models` to be a checkpoint output.

* s/with/type and don't ask for live interactively
2021-10-02 16:12:44 +05:45
Saugat Pachhai 0514db3bfa deps: loose requirements for dataclasses and aiohttp-retry (#6724) 2021-10-01 17:00:03 +05:45
Saugat Pachhai 1e0b2d7af0 benedict: monkeypatch, performance improvements, bugfixes (#6706)
Fixes #6476, fixes #6423 and closes #6484.
2021-10-01 18:16:47 +09:00
Casper da Costa-Luis bfcc8f877c add licence year + company (#6717) 2021-10-01 09:54:55 +02:00
Saugat Pachhai 46ad8745d3 Use latest pygit2 from master for 3.10 (#6723)
* Use latest pygit2 from master for 3.10

* ci: update brew
2021-10-01 11:48:11 +05:45
dependabot[bot] eb13d3ca83 build(deps): Bump google-cloud-storage from 1.42.2 to 1.42.3 (#6722)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.42.2 to 1.42.3.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.42.2...v1.42.3)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  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>
2021-10-01 08:27:40 +05:45
Ruslan Kuprieiev f46beb4958 checkout: move to objects (#6721)
Checkout operation is pretty much the opposite of staging, so it too belongs in objects.
2021-09-30 23:06:03 +03:00
Saugat Pachhai 9a1d74048a cli: add short alias for --show-{csv,json,md} (#6711)
As `--csv`/`--json`/`--md`
2021-09-30 20:38:32 +05:45
dependabot[bot] cce6aed378 build(deps): Bump filelock from 3.1.0 to 3.2.0 (#6710)
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.1.0 to 3.2.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.1.0...3.2.0)

---
updated-dependencies:
- dependency-name: filelock
  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>
2021-09-30 08:49:39 +05:45
Saugat Pachhai 612696cf4a deps: add contextvars as a dependency for Python3.6 (#6705) 2021-09-29 10:39:14 +05:45
Saugat Pachhai (सौगात) 3f5f983f5f ui: import sys on write_json 2021-09-29 09:59:30 +05:45
eggqq007andZhenyu Gao fed9f088ca command: Show dvc list output with pretty JSON format if output to tty (#6647)
1. update ls __init__.py to output pretty JSON format
2. introduce write_json function into ui __init__.py

Co-authored-by: Zhenyu Gao <[email protected]>
2021-09-29 08:44:33 +05:45
Saugat Pachhai 05eaeef012 utils.http: remove open url (#6700)
This was used before on opening http urls as a stream,
but we no longer use those (as we use fsspec's open method).

One thing to note however is that, compared to previous
implementation, fsspec's http filesystem's open is not very
robust to failures.
2021-09-28 19:19:52 +05:45
Saugat Pachhai 48c0527755 exp init: show heading based on the presence of dvc.yaml file (#6701) 2021-09-28 15:11:58 +05:45
Saugat Pachhai 4668a35f64 exp init: support interactive and explicit options (#6681) 2021-09-28 14:36:21 +05:45
dependabot[bot] 8107ec849b build(deps): Bump filelock from 3.0.12 to 3.1.0 (#6698)
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.0.12 to 3.1.0.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Commits](https://github.com/tox-dev/py-filelock/compare/v3.0.12...3.1.0)

---
updated-dependencies:
- dependency-name: filelock
  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>
2021-09-28 14:18:17 +05:45
dependabot[bot]andSaugat Pachhai 19284b3b4c build(deps): Bump urllib3 from 1.26.6 to 1.26.7 (#6697)
* build(deps): Bump urllib3 from 1.26.6 to 1.26.7

Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.6 to 1.26.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.6...1.26.7)

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

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

* Get rid of urllib3 as a test dependency

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Saugat Pachhai <[email protected]>
2021-09-28 13:52:02 +05:45
dependabot[bot] 315d1cde99 build(deps): Bump pytest-xdist from 2.3.0 to 2.4.0 (#6696)
Bumps [pytest-xdist](https://github.com/pytest-dev/pytest-xdist) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/pytest-dev/pytest-xdist/releases)
- [Changelog](https://github.com/pytest-dev/pytest-xdist/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-xdist/compare/v2.3.0...v2.4.0)

---
updated-dependencies:
- dependency-name: pytest-xdist
  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>
2021-09-28 07:50:59 +00:00
dependabot[bot] 1d415efd0b build(deps): Bump google-cloud-storage from 1.42.1 to 1.42.2 (#6635)
Bumps [google-cloud-storage](https://github.com/googleapis/python-storage) from 1.42.1 to 1.42.2.
- [Release notes](https://github.com/googleapis/python-storage/releases)
- [Changelog](https://github.com/googleapis/python-storage/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-storage/compare/v1.42.1...v1.42.2)

---
updated-dependencies:
- dependency-name: google-cloud-storage
  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>
2021-09-28 13:12:18 +05:45
Saugat Pachhai e7c20aa1d8 better exception messages for schema errors (#6679)
* Generate prettier schema error messages

* add test for #6285
2021-09-28 11:12:16 +05:45
Saugat Pachhai 058377f33e ui: support pager universally (#6692) 2021-09-27 18:11:20 +05:45
Saugat Pachhai c7b1b14905 ui: use ui.rich_text instead of rich.text.Text (#6690) 2021-09-27 12:04:19 +05:45
Amey Kulkarni fbd8f5c185 Issue 6651: Added alias ls for dvc list (#6683) 2021-09-25 21:14:11 +03:00
whysage 7fe370b729 remote: add support for Amazon S3 timeout configuration (#6634)
Fixes #6241
2021-09-25 14:06:43 +03:00
Batuhan Taskaya f9df6d2a16 http: allow reading proxies from the current environment; (#6688) 2021-09-25 11:32:44 +02:00
Ruslan Kuprieiev 2e4fb376da system: get rid of speedcopy (#6686)
speedcopy only patches shutil.copyfile, which we don't even use
anywhere because it lacks callbacks/progress and we instead use
our own copyfile implementation, so this is totally useless for
us and can be removed.
2021-09-25 04:42:45 +03:00
Ruslan Kuprieiev 6d6a3f17b1 system: remove legacy code (#6685) 2021-09-25 03:04:54 +03:00
Ruslan Kuprieiev bdaf4aced6 state: cleanup redundant or localfs-specific logic (#6684)
Pre-requisite for making state work with other filesystems.
2021-09-25 02:11:35 +03:00
Batuhan Taskaya 0bf1802d4b ssh: fix path normalization on Windows (#6517) 2021-09-23 18:23:40 +03:00
Batuhan Taskaya 6d3045320b ssh: use a docker-based testing environment (#6677) 2021-09-23 16:00:48 +03:00
Batuhan Taskaya 3469dda8b8 ssh: bundle bcrypt by default (#6678) 2021-09-23 11:00:13 +02:00
Saugat Pachhai eefede92b2 merge yaml parsing and schema validation into one, aka strictyaml.load (#6661) 2021-09-23 00:03:25 +03:00
Ruslan Kuprieiev e295434ec6 checkout: start switching to obj-based workflow (#6648)
* webhdfs: info/checksum: properly raise FileNotFoundError

* checkout: start switching to obj-based workflow

Related #6594
2021-09-22 02:16:29 +03:00
Saugat Pachhai b1f1b01ffc debug: add yappi, callgrind output support (#6664)
* debug: add yappi, callgrind output support

* support custom path and callables
2021-09-21 20:33:08 +03:00
Casper da Costa-Luis f41d7effdb fix dvc[all] (#6657)
* fix dvc[all]

- related https://github.com/iterative/dvc-snap/issues/24

* fix pylint
2021-09-21 18:54:55 +03:00
Saugat Pachhai 6a417885b8 exceptional exceptions (#6654)
* exceptional exceptions

* backward compatibility with previous exceptions

* make linters happy :)
2021-09-21 11:27:57 +05:45
pre-commit-ci[bot] 4221d9f70b [pre-commit.ci] pre-commit autoupdate (#6659)
updates:
- [github.com/ambv/black: 21.8b0 → 21.9b0](https://github.com/ambv/black/compare/21.8b0...21.9b0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2021-09-21 09:28:51 +05:45
Ruslan Kuprieiev 5822d53687 tests: upgrade azurite
```
2021-09-21T00:15:31.6789059Z ____________________ ERROR at setup of test_get_url[azure] _____________________
2021-09-21T00:15:31.6790443Z [gw0] linux -- Python 3.7.12 /opt/hostedtoolcache/Python/3.7.12/x64/bin/python
2021-09-21T00:15:31.6791202Z 
2021-09-21T00:15:31.6792291Z request = <FixtureRequest for <Function test_get_url[azure]>>
2021-09-21T00:15:31.6793561Z 
2021-09-21T00:15:31.6794120Z     def fill(request):
2021-09-21T00:15:31.6794792Z         item = request._pyfuncitem
2021-09-21T00:15:31.6795627Z         fixturenames = getattr(item, "fixturenames", None)
2021-09-21T00:15:31.6797090Z         if fixturenames is None:
2021-09-21T00:15:31.6797922Z             fixturenames = request.fixturenames
2021-09-21T00:15:31.6798497Z     
2021-09-21T00:15:31.6799393Z         if hasattr(item, 'callspec'):
2021-09-21T00:15:31.6800256Z             for param, val in sorted_by_dependency(item.callspec.params, fixturenames):
2021-09-21T00:15:31.6801309Z                 if val is not None and is_lazy_fixture(val):
2021-09-21T00:15:31.6802233Z >                   item.callspec.params[param] = request.getfixturevalue(val.name)
2021-09-21T00:15:31.6802912Z 
2021-09-21T00:15:31.6803937Z /opt/hostedtoolcache/Python/3.7.12/x64/lib/python3.7/site-packages/pytest_lazyfixture.py:35: 
2021-09-21T00:15:31.6804952Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
2021-09-21T00:15:31.6806085Z /home/runner/work/dvc/dvc/tests/remotes/azure.py:102: in azure
2021-09-21T00:15:31.6807090Z     container.get_container_properties()
2021-09-21T00:15:31.6808748Z /opt/hostedtoolcache/Python/3.7.12/x64/lib/python3.7/site-packages/azure/core/tracing/decorator.py:83: in wrapper_use_tracer
2021-09-21T00:15:31.6822551Z     return func(*args, **kwargs)
2021-09-21T00:15:31.6824143Z /opt/hostedtoolcache/Python/3.7.12/x64/lib/python3.7/site-packages/azure/storage/blob/_container_client.py:497: in get_container_properties
2021-09-21T00:15:31.6825038Z     process_storage_error(error)
2021-09-21T00:15:31.6825652Z _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
2021-09-21T00:15:31.6825908Z 
2021-09-21T00:15:31.6826916Z storage_error = HttpResponseError("Operation returned an invalid status 'The value for one of the HTTP headers is not in the correct format.'")
2021-09-21T00:15:31.6827586Z 
2021-09-21T00:15:31.6828351Z     def process_storage_error(storage_error):   # pylint:disable=too-many-statements
2021-09-21T00:15:31.6829086Z         raise_error = HttpResponseError
2021-09-21T00:15:31.6829597Z         serialized = False
2021-09-21T00:15:31.6830066Z         if not storage_error.response:
2021-09-21T00:15:31.6830554Z             raise storage_error
2021-09-21T00:15:31.6831558Z         # If it is one of those three then it has been serialized prior by the generated layer.
2021-09-21T00:15:31.6832480Z         if isinstance(storage_error, (PartialBatchErrorException,
2021-09-21T00:15:31.6833636Z                                       ClientAuthenticationError, ResourceNotFoundError, ResourceExistsError)):
2021-09-21T00:15:31.6834687Z             serialized = True
2021-09-21T00:15:31.6835753Z         error_code = storage_error.response.headers.get('x-ms-error-code')
2021-09-21T00:15:31.6836524Z         error_message = storage_error.message
2021-09-21T00:15:31.6837001Z         additional_data = {}
2021-09-21T00:15:31.6837398Z         error_dict = {}
2021-09-21T00:15:31.6837739Z         try:
2021-09-21T00:15:31.6838506Z             error_body = ContentDecodePolicy.deserialize_from_http_generics(storage_error.response)
2021-09-21T00:15:31.6839345Z             # If it is an XML response
2021-09-21T00:15:31.6839837Z             if isinstance(error_body, Element):
2021-09-21T00:15:31.6840285Z                 error_dict = {
2021-09-21T00:15:31.6840750Z                     child.tag.lower(): child.text
2021-09-21T00:15:31.6841435Z                     for child in error_body
2021-09-21T00:15:31.6841822Z                 }
2021-09-21T00:15:31.6842195Z             # If it is a JSON response
2021-09-21T00:15:31.6842692Z             elif isinstance(error_body, dict):
2021-09-21T00:15:31.6843413Z                 error_dict = error_body.get('error', {})
2021-09-21T00:15:31.6843893Z             elif not error_code:
2021-09-21T00:15:31.6844347Z                 _LOGGER.warning(
2021-09-21T00:15:31.6845611Z                     'Unexpected return type % from ContentDecodePolicy.deserialize_from_http_generics.', type(error_body))
2021-09-21T00:15:31.6846887Z                 error_dict = {'message': str(error_body)}
2021-09-21T00:15:31.6847294Z     
2021-09-21T00:15:31.6847730Z             # If we extracted from a Json or XML response
2021-09-21T00:15:31.6848205Z             if error_dict:
2021-09-21T00:15:31.6848792Z                 error_code = error_dict.get('code')
2021-09-21T00:15:31.6849704Z                 error_message = error_dict.get('message')
2021-09-21T00:15:31.6850578Z                 additional_data = {k: v for k, v in error_dict.items() if k not in {'code', 'message'}}
2021-09-21T00:15:31.6851392Z         except DecodeError:
2021-09-21T00:15:31.6851801Z             pass
2021-09-21T00:15:31.6852290Z     
2021-09-21T00:15:31.6852601Z         try:
2021-09-21T00:15:31.6853156Z             # This check would be unnecessary if we have already serialized the error
2021-09-21T00:15:31.6853821Z             if error_code and not serialized:
2021-09-21T00:15:31.6854423Z                 error_code = StorageErrorCode(error_code)
2021-09-21T00:15:31.6855164Z                 if error_code in [StorageErrorCode.condition_not_met,
2021-09-21T00:15:31.6856229Z                                   StorageErrorCode.blob_overwritten]:
2021-09-21T00:15:31.6856938Z                     raise_error = ResourceModifiedError
2021-09-21T00:15:31.6857739Z                 if error_code in [StorageErrorCode.invalid_authentication_info,
2021-09-21T00:15:31.6858641Z                                   StorageErrorCode.authentication_failed]:
2021-09-21T00:15:31.6859468Z                     raise_error = ClientAuthenticationError
2021-09-21T00:15:31.6860256Z                 if error_code in [StorageErrorCode.resource_not_found,
2021-09-21T00:15:31.6861256Z                                   StorageErrorCode.cannot_verify_copy_source,
2021-09-21T00:15:31.6861998Z                                   StorageErrorCode.blob_not_found,
2021-09-21T00:15:31.6862716Z                                   StorageErrorCode.queue_not_found,
2021-09-21T00:15:31.6863718Z                                   StorageErrorCode.container_not_found,
2021-09-21T00:15:31.6864526Z                                   StorageErrorCode.parent_not_found,
2021-09-21T00:15:31.6865432Z                                   StorageErrorCode.share_not_found]:
2021-09-21T00:15:31.6866549Z                     raise_error = ResourceNotFoundError
2021-09-21T00:15:31.6867609Z                 if error_code in [StorageErrorCode.account_already_exists,
2021-09-21T00:15:31.6868499Z                                   StorageErrorCode.account_being_created,
2021-09-21T00:15:31.6869383Z                                   StorageErrorCode.resource_already_exists,
2021-09-21T00:15:31.6870728Z                                   StorageErrorCode.resource_type_mismatch,
2021-09-21T00:15:31.6871688Z                                   StorageErrorCode.blob_already_exists,
2021-09-21T00:15:31.6872429Z                                   StorageErrorCode.queue_already_exists,
2021-09-21T00:15:31.6873252Z                                   StorageErrorCode.container_already_exists,
2021-09-21T00:15:31.6874124Z                                   StorageErrorCode.container_being_deleted,
2021-09-21T00:15:31.6874922Z                                   StorageErrorCode.queue_being_deleted,
2021-09-21T00:15:31.6875857Z                                   StorageErrorCode.share_already_exists,
2021-09-21T00:15:31.6876608Z                                   StorageErrorCode.share_being_deleted]:
2021-09-21T00:15:31.6877289Z                     raise_error = ResourceExistsError
2021-09-21T00:15:31.6877807Z         except ValueError:
2021-09-21T00:15:31.6878259Z             # Got an unknown error code
2021-09-21T00:15:31.6878648Z             pass
2021-09-21T00:15:31.6878942Z     
2021-09-21T00:15:31.6879424Z         # Error message should include all the error properties
2021-09-21T00:15:31.6879908Z         try:
2021-09-21T00:15:31.6880416Z             error_message += "\nErrorCode:{}".format(error_code.value)
2021-09-21T00:15:31.6881289Z         except AttributeError:
2021-09-21T00:15:31.6883082Z             error_message += "\nErrorCode:{}".format(error_code)
2021-09-21T00:15:31.6883807Z         for name, info in additional_data.items():
2021-09-21T00:15:31.6884446Z             error_message += "\n{}:{}".format(name, info)
2021-09-21T00:15:31.6884932Z     
2021-09-21T00:15:31.6885741Z         # No need to create an instance if it has already been serialized by the generated layer
2021-09-21T00:15:31.6886538Z         if serialized:
2021-09-21T00:15:31.6887019Z             storage_error.message = error_message
2021-09-21T00:15:31.6887526Z             error = storage_error
2021-09-21T00:15:31.6887908Z         else:
2021-09-21T00:15:31.6888499Z             error = raise_error(message=error_message, response=storage_error.response)
2021-09-21T00:15:31.6889380Z         # Ensure these properties are stored in the error instance as well (not just the error message)
2021-09-21T00:15:31.6890078Z         error.error_code = error_code
2021-09-21T00:15:31.6890630Z         error.additional_info = additional_data
2021-09-21T00:15:31.6891956Z         # error.args is what's surfaced on the traceback - show error message in all cases
2021-09-21T00:15:31.6892672Z         error.args = (error.message,)
2021-09-21T00:15:31.6893112Z         try:
2021-09-21T00:15:31.6893784Z             # `from None` prevents us from double printing the exception (suppresses generated layer error context)
2021-09-21T00:15:31.6894873Z >           exec("raise error from None")   # pylint: disable=exec-used # nosec
2021-09-21T00:15:31.6896126Z E           azure.core.exceptions.HttpResponseError: The value for one of the HTTP headers is not in the correct format.
2021-09-21T00:15:31.6897385Z E           RequestId:d83b8102-bde1-4a77-aa7a-b67bbc2c4a80
2021-09-21T00:15:31.6898080Z E           Time:2021-09-21T00:09:53.815Z
2021-09-21T00:15:31.6898578Z E           ErrorCode:InvalidHeaderValue
2021-09-21T00:15:31.6899259Z E           headername:x-ms-version
2021-09-21T00:15:31.6900066Z E           headervalue:2020-10-02
2021-09-21T00:15:31.6900355Z 
2021-09-21T00:15:31.6901781Z /opt/hostedtoolcache/Python/3.7.12/x64/lib/python3.7/site-packages/azure/storage/blob/_shared/response_handlers.py:177: HttpResponseError
```
2021-09-21 03:53:06 +03:00
Batuhan Taskaya a478a0ea98 progress: ignore when set_size callback is called with None (#6612) 2021-09-21 03:34:17 +03:00
Ruslan Kuprieiev e81c88e501 gha: increase timeouts (#6645) 2021-09-19 23:47:15 +03:00
b286c311f4 reincorporate PR 6111 into current master, re: #4420 (#6419)
* rebased PR 6111 onto master

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

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

* use make_tmp_dir fixture in tests

Co-authored-by: U119511 <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Alexander Meier <[email protected]>
Co-authored-by: meierale <[email protected]>
2021-09-18 22:36:50 +03:00
232 changed files with 5786 additions and 4209 deletions
+23
View File
@@ -0,0 +1,23 @@
name: benchmarks
on: [pull_request, workflow_dispatch]
jobs:
bench:
name: run benchmarks
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-18.04]
steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}
- uses: actions/setup-python@v2
with:
python-version: 3.7
- uses: iterative/dvc-bench@master
with:
pytest_options: "-k 'test_init or test_help'"
+18 -6
View File
@@ -10,7 +10,9 @@ jobs:
deb:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/[email protected]
with:
@@ -39,7 +41,9 @@ jobs:
rpm:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/[email protected]
with:
@@ -68,7 +72,9 @@ jobs:
osxpkg:
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/[email protected]
with:
@@ -100,7 +106,9 @@ jobs:
exe:
runs-on: windows-2019
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/[email protected]
with:
@@ -128,7 +136,9 @@ jobs:
pip:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Set up Python 3.7
uses: actions/[email protected]
with:
@@ -136,7 +146,9 @@ jobs:
- name: Install
run: pip install ".[all,tests]"
- name: Build packages
run: ./scripts/build_package.sh
run: |
pip install -U pip
./scripts/build_package.sh
- name: Publish packages to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@master
+20 -21
View File
@@ -16,7 +16,13 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/[email protected]
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}
- uses: actions/[email protected]
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/[email protected]
with:
@@ -24,7 +30,7 @@ jobs:
- name: Install requirements
run: |
pip install wheel
pip install ".[all,tests,terraform]" Pygments collective.checkdocs pre-commit
pip install ".[all,tests,terraform]" pre-commit
- name: install temporary dependencies
run: |
pip install git+https://github.com/isidentical/gdrivefs@service-account
@@ -38,19 +44,16 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-18.04, windows-2019, macos-10.15]
pyv: ["3.6", "3.7", "3.8", "3.9"]
include:
- os: "macos-10.15"
pyv: "3.10.0-rc.1"
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.7", "3.8", "3.9", "3.10"]
exclude:
- os: macos-10.15
pyv: "3.9"
- os: macos-10.15
pyv: "3.7"
# no wheels for pygit2 yet
- os: windows-latest
pyv: "3.10"
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2.4.0
with:
fetch-depth: 0
- name: Set up Python
uses: actions/[email protected]
with:
@@ -64,13 +67,9 @@ jobs:
uses: actions/[email protected]
with:
path: ${{ steps.pip-cache-dir.outputs.dir }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}
key: ${{ runner.os }}-pip-${{ hashFiles('setup.cfg') }}
restore-keys: |
${{ runner.os }}-pip-
- name: install libgit2 on Python3.10 in macOS
if: matrix.pyv == '3.10.0-rc.1' && matrix.os == 'macos-10.15'
run: |
brew install libgit2
- name: install
if: steps.cache.pip-cache-dir.cache-hit != 'true'
run: |
@@ -99,7 +98,7 @@ jobs:
run: |
echo "extra_test_args=--disable-gdrive" >> $GITHUB_ENV
- name: run tests
timeout-minutes: 30
timeout-minutes: 40
env:
GDRIVE_CREDENTIALS_DATA: ${{ secrets.GDRIVE_CREDENTIALS_DATA }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -108,9 +107,9 @@ jobs:
OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET}}
OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT}}
run: >-
python -m tests -n=4
python -m tests -n=auto
--cov-report=xml --cov-report=term
--all ${{ env.extra_test_args }}
--enable-ssh ${{ env.extra_test_args }}
- name: upload coverage report
uses: codecov/[email protected]
with:
+2
View File
@@ -13,6 +13,7 @@ build/
dist/
*.egg-info/
.eggs
hooks/*.pyc
@@ -38,3 +39,4 @@ scripts/ci/gcp-creds.json
.gcp-creds.json
*~
/dvc/_dvc_version.py
+14 -14
View File
@@ -1,12 +1,17 @@
ci:
skip: [pylint, dvc-pre-commit]
skip: [mypy, pylint, dvc-pre-commit]
repos:
- hooks:
- id: black
language_version: python3
repo: https://github.com/ambv/black
rev: 21.8b0
rev: 21.10b0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-toml
- id: check-yaml
- hooks:
- id: codespell
args:
@@ -29,19 +34,14 @@ repos:
- flake8-string-format
repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- hooks:
- id: mypy
language_version: python3
files: ^dvc/
additional_dependencies:
- types-requests
- types-paramiko
- types-tabulate
- types-toml
repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.910'
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy
files: ^dvc/
language: system
types: [python]
- id: pylint
name: pylint
entry: pylint
@@ -54,7 +54,7 @@ repos:
id: beautysh
language_version: python3
repo: https://github.com/lovesegfault/beautysh
rev: v6.1.0
rev: v6.2.1
- hooks:
- args:
- git-hook
-615
View File
@@ -1,615 +0,0 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=pygit2
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
init-hook="import sys;
from pathlib import Path;
from pylint.config import find_pylintrc;
tests_folder = Path(find_pylintrc()).parent / 'tests'
sys.path.append(str(tests_folder));
"
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=pylint_pytest,pylint_plugin_disable
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=format,
# ^ most of them are covered by flake8,
# and are conventions rather than warnings or errors
refactoring, # will be noisy if it starts giving suggestions
spelling,
# design is too-few-* and too-many-* checks, too-much-* for us right now
design,
# following were generated by default
# most of them are redundant Py3 checks for Py2+3 codebase
print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
# custom disables start here
no-self-use,
invalid-name,
# already a check enabled for `wildcard-import`
unused-wildcard-import,
misplaced-comparison-constant,
# probably soon: https://github.com/iterative/dvc/issues/1843
logging-format-interpolation,
logging-fstring-interpolation,
# we don't have proper docstrings yet
# even if we do, we'll be doing it gradually
missing-function-docstring,
missing-module-docstring,
missing-class-docstring,
duplicate-code,
# too noisy, and detects issues even when import is inside a function
# see: https://github.com/PyCQA/pylint/issues/850
cyclic-import,
# isort should cover these:
wrong-import-order,
wrong-import-position,
ungrouped-imports,
multiple-imports,
# we use it to speedup/optimize
import-outside-toplevel,
fixme,
# we use cause of the exception for error message output
# so some of the times we don't use `raise from` to not show the cause
# of the error
raise-missing-from,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member,
no-else-return,
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit
[LOGGING]
# Format style used to check logging format string. `old` means using %
# formatting, while `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package..
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=hdfs.*,pytest.lazy_fixture,logging.TRACE,logger.trace,sys.getwindowsversion
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,Dvcfile
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=azure
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
signature-mutators=funcy.decorators.decorator,
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_|args|kwargs
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=79
# Maximum number of lines in a module.
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_,
s3,
gs,
x, y,
x0, y0,
x1, y1,
dx, dy
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
[DESIGN]
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement.
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled).
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled).
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception".
overgeneral-exceptions=Exception
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Copyright 2017-2021 Iterative, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,3 +1,3 @@
include fastentrypoints.py
include LICENSE
recursive-include requirements *.txt
recursive-include dvc/repo/plots/templates *.json
+40 -1
View File
@@ -1,10 +1,39 @@
from contextlib import ExitStack, contextmanager
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable, Union
if TYPE_CHECKING:
from argparse import Namespace
@contextmanager
def yappi_profile(
path: Union[Callable[[], str], str] = None, wall_clock: bool = True
):
try:
import yappi # pylint: disable=import-error
except ImportError:
print("Failed to run profiler, yappi is not installed")
yield
return
yappi.set_clock_type("wall" if wall_clock else "cpu")
yappi.start()
yield
yappi.stop()
# pylint:disable=no-member
if path:
stats = yappi.get_func_stats()
fpath = path() if callable(path) else path
stats.save(fpath, "callgrind")
else:
yappi.get_func_stats().print_all()
yappi.get_thread_stats().print_all()
yappi.clear_stats()
@contextmanager
def instrument(html_output=False):
"""Run a statistical profiler"""
@@ -70,6 +99,13 @@ def debugtools(args: "Namespace" = None, **kwargs):
stack.enter_context(profile(kw.get("cprofile_dump")))
if kw.get("instrument") or kw.get("instrument_open"):
stack.enter_context(instrument(kw.get("instrument_open", False)))
if kw.get("yappi"):
from datetime import datetime
output = "callgrind.dvc-{0:%Y%m%d}_{0:%H%M%S}.out"
stack.enter_context(
yappi_profile(path=lambda: output.format(datetime.now()))
)
yield
@@ -79,6 +115,9 @@ def add_debugging_flags(parser):
parser.add_argument(
"--cprofile", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument(
"--yappi", action="store_true", default=False, help=SUPPRESS
)
parser.add_argument("--cprofile-dump", help=SUPPRESS)
parser.add_argument(
"--pdb", action="store_true", default=False, help=SUPPRESS
+3 -3
View File
@@ -68,7 +68,7 @@ def send(path):
url = "https://analytics.dvc.org"
headers = {"content-type": "application/json"}
with open(path) as fobj:
with open(path, encoding="utf-8") as fobj:
report = json.load(fobj)
report.update(_runtime_info())
@@ -173,13 +173,13 @@ def _find_or_create_user_id():
try:
with Lock(lockfile):
try:
with open(fname) as fobj:
with open(fname, encoding="utf-8") as fobj:
user_id = json.load(fobj)["user_id"]
except (FileNotFoundError, ValueError, KeyError):
user_id = str(uuid.uuid4())
with open(fname, "w") as fobj:
with open(fname, "w", encoding="utf-8") as fobj:
json.dump({"user_id": user_id}, fobj)
return user_id
+1 -1
View File
@@ -113,7 +113,7 @@ def make_checkpoint():
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
)
with builtins.open(signal_file, "w") as fobj:
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
# NOTE: force flushing/writing empty file to disk, otherwise when
# run in certain contexts (pytest) file may not actually be written
fobj.write("")
+2 -3
View File
@@ -103,9 +103,8 @@ class CmdDAG(CmdBase):
if self.args.dot:
ui.write(_show_dot(G))
else:
from dvc.utils.pager import pager
pager(_show_ascii(G))
with ui.pager():
ui.write(_show_ascii(G))
return 0
+1
View File
@@ -393,6 +393,7 @@ def add_parser(subparsers, _parent_parser):
help="Show status of all stages in the specified directory.",
)
status_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
+8 -6
View File
@@ -1,5 +1,4 @@
import argparse
import json
import logging
import os
@@ -16,7 +15,7 @@ def _digest(checksum):
return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
def _show_md(diff, show_hash=False, hide_missing=False):
def _show_markdown(diff, show_hash=False, hide_missing=False):
headers = ["Status", "Hash", "Path"] if show_hash else ["Status", "Path"]
rows = []
statuses = ["added", "deleted", "renamed", "modified"]
@@ -142,10 +141,10 @@ class CmdDiff(CmdBase):
del entry["hash"]
diff[key] = entries
if self.args.show_json:
ui.write(json.dumps(diff))
elif self.args.show_md:
_show_md(diff, show_hash, hide_missing)
if self.args.json:
ui.write_json(diff)
elif self.args.markdown:
_show_markdown(diff, show_hash, hide_missing)
elif diff:
self._show_diff(diff, hide_missing)
@@ -188,6 +187,7 @@ def add_parser(subparsers, parent_parser):
nargs="?",
)
diff_parser.add_argument(
"--json",
"--show-json",
help="Format the output into a JSON",
action="store_true",
@@ -200,9 +200,11 @@ def add_parser(subparsers, parent_parser):
default=False,
)
diff_parser.add_argument(
"--md",
"--show-md",
help="Show tabulated output in the Markdown format (GFM).",
action="store_true",
dest="markdown",
default=False,
)
diff_parser.add_argument(
+149 -78
View File
@@ -1,14 +1,12 @@
import argparse
import logging
import os
from collections import Counter, OrderedDict, defaultdict
from datetime import date, datetime
from fnmatch import fnmatch
from typing import TYPE_CHECKING, Dict, Iterable, Optional
from funcy import lmap
from funcy import compact, lmap
from dvc import prompt
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.command.metrics import DEFAULT_PRECISION
@@ -20,9 +18,8 @@ from dvc.utils.flatten import flatten
from dvc.utils.serialize import encode_exception
if TYPE_CHECKING:
from rich.text import Text
from dvc.compare import TabularData
from dvc.ui import RichText
logger = logging.getLogger(__name__)
@@ -261,11 +258,12 @@ def _sort_column(sort_by, metric_names, param_names):
def _sort_exp(experiments, sort_path, sort_name, typ, reverse):
def _sort(item):
rev, exp = item
tip = exp.get("checkpoint_tip")
exp_data = exp.get("data", {})
tip = exp_data.get("checkpoint_tip")
if tip and tip != rev:
# Sort checkpoint experiments by tip commit
return _sort((tip, experiments[tip]))
data = exp.get(typ, {}).get(sort_path, {})
data = exp_data.get(typ, {}).get(sort_path, {}).get("data", {})
val = flatten(data).get(sort_name)
return val is None, val
@@ -292,8 +290,6 @@ def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
from rich.text import Text
from dvc.compare import _format_field, with_value
if not items:
@@ -308,10 +304,10 @@ def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
item.get(name),
FILL_VALUE_ERRORED if data.get("error", None) else fill_value,
)
# wrap field data in rich.Text, otherwise rich may
# wrap field data in ui.rich_text, otherwise rich may
# interpret unescaped braces from list/dict types as rich
# markup tags
row.append(Text(str(_format_field(value, precision))))
row.append(ui.rich_text(str(_format_field(value, precision))))
def _parse_filter_list(param_list):
@@ -362,17 +358,15 @@ def experiments_table(
return td
def prepare_exp_id(kwargs) -> "Text":
from rich.text import Text
def prepare_exp_id(kwargs) -> "RichText":
exp_name = kwargs["Experiment"]
rev = kwargs["rev"]
typ = kwargs.get("typ", "baseline")
if typ == "baseline" or not exp_name:
text = Text(exp_name or rev)
text = ui.rich_text(exp_name or rev)
else:
text = Text.assemble(rev, " [", (exp_name, "bold"), "]")
text = ui.rich_text.assemble(rev, " [", (exp_name, "bold"), "]")
parent = kwargs.get("parent")
suff = f" ({parent})" if parent else ""
@@ -380,7 +374,7 @@ def prepare_exp_id(kwargs) -> "Text":
tree = experiment_types[typ]
pref = f"{tree} " if tree else ""
return Text(pref) + text
return ui.rich_text(pref) + text
def baseline_styler(typ):
@@ -388,7 +382,12 @@ def baseline_styler(typ):
def show_experiments(
all_experiments, pager=True, no_timestamp=False, show_csv=False, **kwargs
all_experiments,
pager=True,
no_timestamp=False,
csv=False,
markdown=False,
**kwargs,
):
from funcy.seqs import flatten as flatten_list
@@ -444,7 +443,7 @@ def show_experiments(
row_styles = lmap(baseline_styler, td.column("typ"))
if not show_csv:
if not csv:
merge_headers = ["Experiment", "rev", "typ", "parent"]
td.column("Experiment")[:] = map(
prepare_exp_id, td.as_dict(merge_headers)
@@ -478,7 +477,8 @@ def show_experiments(
rich_table=True,
header_styles=styles,
row_styles=row_styles,
show_csv=show_csv,
csv=csv,
markdown=markdown,
)
@@ -511,18 +511,16 @@ class CmdExperimentsShow(CmdBase):
logger.exception("failed to show experiments")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(all_experiments, default=_format_json))
if self.args.json:
ui.write_json(all_experiments, default=_format_json)
else:
precision = (
self.args.precision or None
if self.args.show_csv
if self.args.csv
else DEFAULT_PRECISION
)
fill_value = "" if self.args.show_csv else FILL_VALUE
iso = True if self.args.show_csv else False
fill_value = "" if self.args.csv else FILL_VALUE
iso = True if self.args.csv else False
show_experiments(
all_experiments,
@@ -537,7 +535,8 @@ class CmdExperimentsShow(CmdBase):
fill_value=fill_value,
iso=iso,
pager=not self.args.no_pager,
show_csv=self.args.show_csv,
csv=self.args.csv,
markdown=self.args.markdown,
)
return 0
@@ -566,10 +565,8 @@ class CmdExperimentsDiff(CmdBase):
logger.exception("failed to show experiments diff")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(diff))
if self.args.json:
ui.write_json(diff)
else:
from dvc.compare import show_diff
@@ -584,11 +581,13 @@ class CmdExperimentsDiff(CmdBase):
show_diff(
diff[key],
title=title,
markdown=self.args.show_md,
markdown=self.args.markdown,
no_path=self.args.no_path,
old=self.args.old,
on_empty_diff="diff not supported",
precision=precision if key == "metrics" else None,
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
)
return 0
@@ -669,7 +668,7 @@ class CmdExperimentsGC(CmdRepro):
logger.warning(msg)
msg = "Are you sure you want to proceed?"
if not self.args.force and not prompt.confirm(msg):
if not self.args.force and not ui.confirm(msg):
return 1
removed = self.repo.experiments.gc(
@@ -798,51 +797,84 @@ class CmdExperimentsInit(CmdBase):
DEFAULT_PARAMS = "params.yaml"
PLOTS = "plots"
DVCLIVE = "dvclive"
DEFAULT_NAME = "default"
DEFAULTS = {
"code": CODE,
"data": DATA,
"models": MODELS,
"metrics": DEFAULT_METRICS,
"params": DEFAULT_PARAMS,
"plots": PLOTS,
"live": DVCLIVE,
}
EXP_LINK = (
"https://dvc.org/doc"
"/user-guide/experiment-management/running-experiments"
)
def run(self):
from dvc.command.stage import parse_cmd
cmd = parse_cmd(self.args.cmd)
if not cmd:
if not self.args.interactive and not cmd:
raise InvalidArgumentError("command is not specified")
if self.args.interactive:
raise NotImplementedError(
"'-i/--interactive' is not implemented yet."
)
if self.args.explicit:
raise NotImplementedError("'--explicit' is not implemented yet.")
if self.args.template:
raise NotImplementedError("template is not supported yet.")
from dvc.utils.serialize import LOADERS
from dvc.repo.experiments.init import init
code = self.args.code or self.CODE
data = self.args.data or self.DATA
models = self.args.models or self.MODELS
metrics = self.args.metrics or self.DEFAULT_METRICS
params_path = self.args.params or self.DEFAULT_PARAMS
plots = self.args.plots or self.PLOTS
dvclive = self.args.live or self.DVCLIVE
defaults = {}
if not self.args.explicit:
config = self.repo.config["exp"]
defaults.update({**self.DEFAULTS, **config})
_, ext = os.path.splitext(params_path)
params = list(LOADERS[ext](params_path))
name = self.args.name or self.DEFAULT_NAME
stage = self.repo.stage.add(
name=name,
cmd=cmd,
deps=[code, data],
outs=[models],
params=[{params_path: params}],
metrics_no_cache=[metrics],
plots_no_cache=[plots],
live=dvclive,
force=True,
cli_args = compact(
{
"cmd": cmd,
"code": self.args.code,
"data": self.args.data,
"models": self.args.models,
"metrics": self.args.metrics,
"params": self.args.params,
"plots": self.args.plots,
"live": self.args.live,
}
)
initialized_stage = init(
self.repo,
name=self.args.name,
type=self.args.type,
defaults=defaults,
overrides=cli_args,
interactive=self.args.interactive,
force=self.args.force,
)
name = self.args.name or self.args.type
text = ui.rich_text.assemble(
"\n" if self.args.interactive else "",
"Created ",
(name, "bright_blue"),
" stage in ",
("dvc.yaml", "green"),
".",
)
if not self.args.run:
text.append_text(
ui.rich_text.assemble(
" To run, use ",
('"dvc exp run"', "green"),
".\nSee ",
(self.EXP_LINK, "repr.url"),
".",
)
)
ui.write(text, styled=True)
if self.args.run:
return self.repo.experiments.run(targets=[stage.addressing])
return self.repo.experiments.run(
targets=[initialized_stage.addressing]
)
return 0
@@ -951,7 +983,10 @@ def add_parser(subparsers, parent_parser):
)
experiments_show_parser.add_argument(
"--sort-order",
help="Sort order to use with --sort-by.",
help=(
"Sort order to use with --sort-by."
" Defaults to ascending ('asc')."
),
choices=("asc", "desc"),
default="asc",
)
@@ -968,17 +1003,27 @@ def add_parser(subparsers, parent_parser):
help="Always show git commit SHAs instead of branch/tag names.",
)
experiments_show_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Print output in JSON format instead of a human-readable table.",
)
experiments_show_parser.add_argument(
"--csv",
"--show-csv",
action="store_true",
default=False,
help="Print output in csv format instead of a human-readable table.",
)
experiments_show_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
help="Show tabulated output in the Markdown format (GFM).",
)
experiments_show_parser.add_argument(
"--precision",
type=int,
@@ -1042,22 +1087,25 @@ def add_parser(subparsers, parent_parser):
help="Show only params that are stage dependencies.",
)
experiments_diff_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
experiments_diff_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
help="Show tabulated output in the Markdown format (GFM).",
)
experiments_diff_parser.add_argument(
"--old",
action="store_true",
default=False,
help="Show old metric/param value.",
help="Show old ('a_rev') metric/param value.",
)
experiments_diff_parser.add_argument(
"--no-path",
@@ -1385,10 +1433,17 @@ def add_parser(subparsers, parent_parser):
help="Prompt for values that are not provided",
)
experiments_init_parser.add_argument(
"--template", help="Stage template to use to fill with provided values"
"-f",
"--force",
action="store_true",
default=False,
help="Overwrite existing stage",
)
experiments_init_parser.add_argument(
"--explicit", help="Only use the path values explicitly provided"
"--explicit",
action="store_true",
default=False,
help="Only use the path values explicitly provided",
)
experiments_init_parser.add_argument(
"--name", "-n", help="Name of the stage to create"
@@ -1396,29 +1451,45 @@ def add_parser(subparsers, parent_parser):
experiments_init_parser.add_argument(
"--code",
help="Path to the source file or directory "
"which your experiments depend",
"which your experiments depend"
f" (default: {CmdExperimentsInit.CODE})",
)
experiments_init_parser.add_argument(
"--data",
help="Path to the data file or directory "
"which your experiments depend",
"which your experiments depend"
f" (default: {CmdExperimentsInit.DATA})",
)
experiments_init_parser.add_argument(
"--models",
help="Path to the model file or directory for your experiments",
help="Path to the model file or directory for your experiments"
f" (default: {CmdExperimentsInit.MODELS})",
)
experiments_init_parser.add_argument(
"--params", help="Path to the parameters file for your experiments"
"--params",
help="Path to the parameters file for your experiments"
f" (default: {CmdExperimentsInit.DEFAULT_PARAMS})",
)
experiments_init_parser.add_argument(
"--metrics", help="Path to the metrics file for your experiments"
"--metrics",
help="Path to the metrics file for your experiments"
f" (default: {CmdExperimentsInit.DEFAULT_METRICS})",
)
experiments_init_parser.add_argument(
"--plots",
help="Path to the plots file or directory for your experiments",
help="Path to the plots file or directory for your experiments"
f" (default: {CmdExperimentsInit.PLOTS})",
)
experiments_init_parser.add_argument(
"--live", help="Path to log dvclive outputs for your experiments"
"--live",
help="Path to log dvclive outputs for your experiments"
f" (default: {CmdExperimentsInit.DVCLIVE})",
)
experiments_init_parser.add_argument(
"--type",
choices=["default", "live"],
default="default",
help="Select type of stage to create (default: %(default)s)",
)
experiments_init_parser.set_defaults(func=CmdExperimentsInit)
+7 -5
View File
@@ -34,10 +34,8 @@ class CmdList(CmdBaseNoRepo):
recursive=self.args.recursive,
dvc_only=self.args.dvc_only,
)
if self.args.show_json:
import json
ui.write(json.dumps(entries))
if self.args.json:
ui.write_json(entries)
elif entries:
entries = _prettify(entries, with_color=True)
ui.write("\n".join(entries))
@@ -54,6 +52,7 @@ def add_parser(subparsers, parent_parser):
)
list_parser = subparsers.add_parser(
"list",
aliases=["ls"],
parents=[parent_parser],
description=append_doc_link(LIST_HELP, "list"),
help=LIST_HELP,
@@ -70,7 +69,10 @@ def add_parser(subparsers, parent_parser):
"--dvc-only", action="store_true", help="Show only DVC outputs."
)
list_parser.add_argument(
"--show-json", action="store_true", help="Show output in JSON format."
"--json",
"--show-json",
action="store_true",
help="Show output in JSON format.",
)
list_parser.add_argument(
"--rev",
+97
View File
@@ -3,6 +3,7 @@ import argparse
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.config import ConfigError
from dvc.exceptions import DvcException
from dvc.ui import ui
from dvc.utils import format_link
@@ -104,6 +105,54 @@ class CmdMachineModify(CmdMachineConfig):
return 0
class CmdMachineRename(CmdBase):
def _check_exists(self, conf):
if self.args.name not in conf["machine"]:
raise ConfigError(f"machine '{self.args.name}' doesn't exist.")
def _rename_default(self, conf):
if conf["core"].get("machine") == self.args.name:
conf["core"]["machine"] = self.args.new
def _check_before_rename(self):
from dvc.machine import validate_name
validate_name(self.args.new)
all_config = self.config.load_config_to_level(None)
if self.args.new in all_config.get("machine", {}):
raise ConfigError(
"Rename failed. Machine '{}' already exists.".format(
self.args.new
)
)
ui.write(f"Rename machine '{self.args.name}' to '{self.args.new}'.")
def run(self):
self._check_before_rename()
with self.config.edit(self.args.level) as conf:
self._check_exists(conf)
conf["machine"][self.args.new] = conf["machine"][self.args.name]
try:
self.repo.machine.rename(self.args.name, self.args.new)
except DvcException as error:
del conf["machine"][self.args.new]
raise ConfigError("terraform rename failed") from error
del conf["machine"][self.args.name]
self._rename_default(conf)
up_to_level = self.args.level or "repo"
for level in reversed(self.config.LEVELS):
if level == up_to_level:
break
with self.config.edit(level) as level_conf:
self._rename_default(level_conf)
return 0
class CmdMachineDefault(CmdMachineConfig):
def run(self):
if self.args.name is None and not self.args.unset:
@@ -143,6 +192,29 @@ class CmdMachineCreate(CmdBase):
return 0
class CmdMachineStatus(CmdBase):
SHOWN_FIELD = [
"name",
"cloud",
"instance_ip",
"instance_type",
"instance_hdd_size",
"instance_gpu",
]
def run(self):
if self.repo.machine is None:
raise MachineDisabledError
all_status = self.repo.machine.status(self.args.name)
for i, status in enumerate(all_status, start=1):
ui.write(f"instance_num_{i}:")
for field in self.SHOWN_FIELD:
value = status.get(field, None)
ui.write(f"\t{field:20}: {value}")
return 0
class CmdMachineDestroy(CmdBase):
def run(self):
if self.repo.machine is None:
@@ -280,6 +352,18 @@ def add_parser(subparsers, parent_parser):
)
machine_modify_parser.set_defaults(func=CmdMachineModify)
machine_RENAME_HELP = "Rename a machine "
machine_rename_parser = machine_subparsers.add_parser(
"rename",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(machine_RENAME_HELP, "remote/rename"),
help=machine_RENAME_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_rename_parser.add_argument("name", help="Machine to be renamed")
machine_rename_parser.add_argument("new", help="New name of the machine")
machine_rename_parser.set_defaults(func=CmdMachineRename)
machine_REMOVE_HELP = "Remove an machine."
machine_remove_parser = machine_subparsers.add_parser(
"remove",
@@ -306,6 +390,19 @@ def add_parser(subparsers, parent_parser):
)
machine_create_parser.set_defaults(func=CmdMachineCreate)
machine_STATUS_HELP = "List the status of a running machine."
machine_status_parser = machine_subparsers.add_parser(
"status",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(machine_STATUS_HELP, "machine/status"),
help=machine_STATUS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
machine_status_parser.add_argument(
"name", help="Name of the running machine."
)
machine_status_parser.set_defaults(func=CmdMachineStatus)
machine_DESTROY_HELP = "Destroy an machine instance."
machine_destroy_parser = machine_subparsers.add_parser(
"destroy",
+14 -10
View File
@@ -31,16 +31,14 @@ class CmdMetricsShow(CmdMetricsBase):
logger.exception("")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(metrics, default=encode_exception))
if self.args.json:
ui.write_json(metrics, default=encode_exception)
else:
from dvc.compare import show_metrics
show_metrics(
metrics,
markdown=self.args.show_md,
markdown=self.args.markdown,
all_branches=self.args.all_branches,
all_tags=self.args.all_tags,
all_commits=self.args.all_commits,
@@ -65,20 +63,20 @@ class CmdMetricsDiff(CmdMetricsBase):
logger.exception("failed to show metrics diff")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(diff))
if self.args.json:
ui.write_json(diff)
else:
from dvc.compare import show_diff
show_diff(
diff,
title="Metric",
markdown=self.args.show_md,
markdown=self.args.markdown,
no_path=self.args.no_path,
precision=self.args.precision or DEFAULT_PRECISION,
round_digits=True,
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
)
return 0
@@ -140,15 +138,18 @@ def add_parser(subparsers, parent_parser):
help="Show metrics for all commits.",
)
metrics_show_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_show_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
help="Show tabulated output in the Markdown format (GFM).",
)
metrics_show_parser.add_argument(
@@ -220,15 +221,18 @@ def add_parser(subparsers, parent_parser):
help="Show unchanged metrics as well.",
)
metrics_diff_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
metrics_diff_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
help="Show tabulated output in the Markdown format (GFM).",
)
metrics_diff_parser.add_argument(
+8 -5
View File
@@ -25,19 +25,19 @@ class CmdParamsDiff(CmdBase):
logger.exception("failed to show params diff")
return 1
if self.args.show_json:
import json
ui.write(json.dumps(diff))
if self.args.json:
ui.write_json(diff)
else:
from dvc.compare import show_diff
show_diff(
diff,
title="Param",
markdown=self.args.show_md,
markdown=self.args.markdown,
no_path=self.args.no_path,
show_changes=False,
a_rev=self.args.a_rev,
b_rev=self.args.b_rev,
)
return 0
@@ -103,15 +103,18 @@ def add_parser(subparsers, parent_parser):
help="Show only params that are stage dependencies.",
)
params_diff_parser.add_argument(
"--json",
"--show-json",
action="store_true",
default=False,
help="Show output in JSON format.",
)
params_diff_parser.add_argument(
"--md",
"--show-md",
action="store_true",
default=False,
dest="markdown",
help="Show tabulated output in the Markdown format (GFM).",
)
params_diff_parser.add_argument(
+9 -2
View File
@@ -68,8 +68,12 @@ class CmdPlots(CmdBase):
if self.args.open:
import webbrowser
from platform import uname
opened = webbrowser.open(index_path)
if "Microsoft" in uname().release:
url = Path(rel) / "index.html"
opened = webbrowser.open(url)
if not opened:
ui.error_write(
"Failed to open. Please try opening it manually."
@@ -183,7 +187,10 @@ def add_parser(subparsers, parent_parser):
_add_output_arguments(plots_diff_parser)
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
PLOTS_MODIFY_HELP = "Modify display properties of plot metrics files."
PLOTS_MODIFY_HELP = (
"Modify display properties of data-series plots "
"(has no effect on image-type plots)."
)
plots_modify_parser = plots_subparsers.add_parser(
"modify",
parents=[parent_parser],
+1 -1
View File
@@ -86,7 +86,7 @@ class CmdRemoteDefault(CmdRemote):
if self.args.name is None and not self.args.unset:
conf = self.config.read(self.args.level)
try:
print(conf["core"]["remote"])
ui.write(conf["core"]["remote"])
except KeyError:
ui.write("No default remote set")
return 1
+2 -4
View File
@@ -69,10 +69,8 @@ class CmdDataStatus(CmdDataBase):
if self.args.quiet:
return bool(st)
if self.args.show_json:
import json
ui.write(json.dumps(st))
if self.args.json:
ui.write_json(st)
return 0
if st:
+42 -4
View File
@@ -12,6 +12,7 @@ from typing import (
Mapping,
MutableSequence,
Sequence,
Set,
Tuple,
Union,
overload,
@@ -168,7 +169,7 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
def render(self, **kwargs: Any):
from dvc.ui import ui
if kwargs.pop("show_csv", False):
if kwargs.pop("csv", False):
ui.write(self.to_csv(), end="")
else:
ui.table(self, headers=self.keys(), **kwargs)
@@ -181,6 +182,35 @@ class TabularData(MutableSequence[Sequence["CellT"]]):
{k: self._columns[k][i] for k in keys} for i in range(len(self))
]
def dropna(self, axis: str = "rows"):
if axis not in ["rows", "cols"]:
raise ValueError(
f"Invalid 'axis' value {axis}."
"Choose one of ['rows', 'cols']"
)
to_drop: Set = set()
for n_row, row in enumerate(self):
for n_col, col in enumerate(row):
if col == self._fill_value:
if axis == "rows":
to_drop.add(n_row)
break
else:
to_drop.add(self.keys()[n_col])
if axis == "rows":
for name in self.keys():
self._columns[name] = Column(
[
x
for n, x in enumerate(self._columns[name])
if n not in to_drop
]
)
else:
self.drop(*to_drop)
def _normalize_float(val: float, precision: int):
return f"{val:.{precision}g}"
@@ -211,8 +241,12 @@ def diff_table(
precision: int = None,
round_digits: bool = False,
on_empty_diff: str = None,
a_rev: str = None,
b_rev: str = None,
) -> TabularData:
headers: List[str] = ["Path", title, "Old", "New", "Change"]
a_rev = a_rev or "HEAD"
b_rev = b_rev or "workspace"
headers: List[str] = ["Path", title, a_rev, b_rev, "Change"]
fill_value = "-"
td = TabularData(headers, fill_value=fill_value)
@@ -240,8 +274,8 @@ def diff_table(
td.drop("Change")
if not old:
td.drop("Old")
td.rename("New", "Value")
td.drop(a_rev)
td.rename(b_rev, "Value")
return td
@@ -256,6 +290,8 @@ def show_diff(
round_digits: bool = False,
on_empty_diff: str = None,
markdown: bool = False,
a_rev: str = None,
b_rev: str = None,
) -> None:
td = diff_table(
diff,
@@ -266,6 +302,8 @@ def show_diff(
precision=precision,
round_digits=round_digits,
on_empty_diff=on_empty_diff,
a_rev=a_rev,
b_rev=b_rev,
)
td.render(markdown=markdown)
+1 -1
View File
@@ -141,7 +141,7 @@ class Config(dict):
dvc.config.Config: config object.
"""
config_file = os.path.join(dvc_dir, Config.CONFIG)
open(config_file, "w+").close()
open(config_file, "w+", encoding="utf-8").close()
return Config(dvc_dir)
def load(self, validate=True, config=None):
+21 -4
View File
@@ -156,6 +156,8 @@ SCHEMA = {
"grant_write_acp": str,
"grant_full_control": str,
"cache_regions": bool,
"read_timeout": Coerce(int),
"connect_timeout": Coerce(int),
**REMOTE_COMMON,
},
"gs": {
@@ -177,10 +179,12 @@ SCHEMA = {
},
"hdfs": {"user": str, "kerb_ticket": str, **REMOTE_COMMON},
"webhdfs": {
"hdfscli_config": str,
"webhdfs_token": str,
"user": str,
"webhdfs_alias": str,
"kerberos": Bool,
"kerberos_principal": str,
"proxy_to": str,
"ssl_verify": Any(Bool, str),
"token": str,
"use_https": Bool,
**REMOTE_COMMON,
},
"azure": {
@@ -223,9 +227,13 @@ SCHEMA = {
)
},
"state": {
"dir": str,
"row_limit": All(Coerce(int), Range(1)), # obsoleted
"row_cleanup_quota": All(Coerce(int), Range(0, 100)), # obsoleted
},
"index": {
"dir": str,
},
"machine": {
str: {
"cloud": All(Lower, Choices("aws", "azure")),
@@ -250,4 +258,13 @@ SCHEMA = {
Optional("parametrization", default=True): Bool,
},
"plots": {"html_template": str},
"exp": {
"code": str,
"data": str,
"models": str,
"metrics": str,
"params": str,
"plots": str,
"live": str,
},
}
+4 -5
View File
@@ -12,8 +12,6 @@ from dvc.utils import fix_env, is_binary
logger = logging.getLogger(__name__)
CREATE_NO_WINDOW = 0x08000000 # only available since Python 3.7 in subprocess.
def _popen(cmd, **kwargs):
prefix = [sys.executable]
@@ -28,6 +26,7 @@ def _popen(cmd, **kwargs):
def _spawn_windows(cmd, env):
from subprocess import (
CREATE_NEW_PROCESS_GROUP,
CREATE_NO_WINDOW,
STARTF_USESHOWWINDOW,
STARTUPINFO,
)
@@ -49,17 +48,17 @@ def _spawn_posix(cmd, env):
# with PyInstaller has trouble with SystemExit exception and throws
# errors such as "[26338] Failed to execute script __main__"
try:
pid = os.fork()
pid = os.fork() # pylint: disable=no-member
if pid > 0:
return
except OSError:
logger.exception("failed at first fork")
os._exit(1) # pylint: disable=protected-access
os.setsid()
os.setsid() # pylint: disable=no-member
try:
pid = os.fork()
pid = os.fork() # pylint: disable=no-member
if pid > 0:
os._exit(0) # pylint: disable=protected-access
except OSError:
+1 -1
View File
@@ -59,7 +59,7 @@ class DataCloud:
from dvc.objects.db import get_odb
cls, config, path_info = get_cloud_fs(self.repo, name=name)
config["tmp_dir"] = self.repo.tmp_dir
config["tmp_dir"] = self.repo.index_db_dir
return get_odb(cls(**config), path_info, **config)
def push(
+20
View File
@@ -1,3 +1,4 @@
import logging
import os
from collections import defaultdict
@@ -10,6 +11,8 @@ from dvc.utils.serialize import LOADERS, ParseError
from .base import Dependency
logger = logging.getLogger(__name__)
class MissingParamsError(DvcException):
pass
@@ -133,3 +136,20 @@ class ParamsDependency(Dependency):
)
return HashInfo(self.PARAM_PARAMS, info)
def save(self):
if not self.exists:
raise self.DoesNotExistError(self)
if not self.isfile and not self.isdir:
raise self.IsNotFileOrDirError(self)
if self.is_empty:
logger.warning(f"'{self}' is empty.")
self.ignore()
if self.metric or self.plot:
self.verify_metric()
self.hash_info = self.get_hash()
+4 -6
View File
@@ -63,7 +63,7 @@ class RepoDependency(Dependency):
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
def download(self, to, jobs=None):
from dvc.checkout import checkout
from dvc.objects.checkout import checkout
for odb, objs in self.get_used_objs().items():
self.repo.cloud.pull(objs, jobs=jobs, odb=odb)
@@ -132,7 +132,7 @@ class RepoDependency(Dependency):
pass
try:
staging, staged_obj = stage(
staging, _, staged_obj = stage(
local_odb,
path_info,
repo.repo_fs,
@@ -148,9 +148,7 @@ class RepoDependency(Dependency):
self._staged_objs[rev] = staged_obj
used_obj_ids[staging].add(staged_obj.hash_info)
if isinstance(staged_obj, Tree):
used_obj_ids[staging].update(
entry.hash_info for _, entry in staged_obj
)
used_obj_ids[staging].update(oid for _, _, oid in staged_obj)
return used_obj_ids, staged_obj
def _check_circular_import(self, odb, obj_ids):
@@ -166,7 +164,7 @@ class RepoDependency(Dependency):
for hash_info in obj_ids:
if hash_info.isdir:
tree = Tree.load(odb, hash_info)
yield from (odb.get(entry.hash_info) for _, entry in tree)
yield from (odb.get(oid) for _, _, oid in tree)
else:
yield odb.get(hash_info)
+34 -52
View File
@@ -1,10 +1,7 @@
import collections
import contextlib
import logging
import os
from typing import TYPE_CHECKING, Any, Union
from voluptuous import MultipleInvalid
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union
from dvc.exceptions import DvcException
from dvc.parsing.versions import LOCKFILE_VERSION, SCHEMA_KWD
@@ -12,23 +9,18 @@ from dvc.stage import serialize
from dvc.stage.exceptions import (
StageFileBadNameError,
StageFileDoesNotExistError,
StageFileFormatError,
StageFileIsNotDvcFileError,
)
from dvc.types import AnyPath
from dvc.utils import relpath
from dvc.utils.collections import apply_diff
from dvc.utils.serialize import (
dump_yaml,
modify_yaml,
parse_yaml,
parse_yaml_for_update,
)
from dvc.utils.serialize import dump_yaml, modify_yaml
if TYPE_CHECKING:
from dvc.repo import Repo
logger = logging.getLogger(__name__)
_T = TypeVar("_T")
DVC_FILE = "Dvcfile"
DVC_FILE_SUFFIX = ".dvc"
@@ -45,10 +37,6 @@ class FileIsGitIgnored(DvcException):
)
class LockfileCorruptedError(DvcException):
pass
class ParametrizedDumpError(DvcException):
pass
@@ -96,7 +84,7 @@ def check_dvcfile_path(repo, path):
class FileMixin:
SCHEMA = None
SCHEMA: Callable[[_T], _T]
def __init__(self, repo, path, verify=True, **kwargs):
self.repo = repo
@@ -138,7 +126,11 @@ class FileMixin:
if self._is_git_ignored():
raise FileIsGitIgnored(self.path)
def _load(self):
def load(self, **kwargs: Any) -> Any:
d, _ = self._load(**kwargs)
return d
def _load(self, **kwargs: Any) -> Tuple[Any, str]:
# it raises the proper exceptions by priority:
# 1. when the file doesn't exists
# 2. filename is not a DVC file
@@ -155,18 +147,23 @@ class FileMixin:
raise StageFileIsNotDvcFileError(self.path)
self._check_gitignored()
with self.repo.fs.open(self.path, encoding="utf-8") as fd:
stage_text = fd.read()
d = parse_yaml(stage_text, self.path)
return self.validate(d, self.relpath), stage_text
return self._load_yaml(**kwargs)
@classmethod
def validate(cls, d, fname=None):
assert isinstance(cls.SCHEMA, collections.abc.Callable)
try:
return cls.SCHEMA(d) # pylint: disable=not-callable
except MultipleInvalid as exc:
raise StageFileFormatError(f"'{fname}' format error: {exc}")
def validate(cls, d: _T, fname: str = None) -> _T:
from dvc.utils.strictyaml import validate
return validate(d, cls.SCHEMA, path=fname) # type: ignore[arg-type]
def _load_yaml(self, **kwargs: Any) -> Tuple[Any, str]:
from dvc.utils import strictyaml
return strictyaml.load(
self.path,
self.SCHEMA, # type: ignore[arg-type]
self.repo.fs,
**kwargs,
)
def remove(self, force=False): # pylint: disable=unused-argument
with contextlib.suppress(FileNotFoundError):
@@ -298,10 +295,7 @@ class PipelineFile(FileMixin):
if not self.exists():
return
with open(self.path, encoding="utf-8") as f:
d = parse_yaml_for_update(f.read(), self.path)
self.validate(d, self.path)
d, _ = self._load_yaml(round_trip=True)
if stage.name not in d.get("stages", {}):
return
@@ -343,34 +337,25 @@ def migrate_lock_v1_to_v2(d, version_info):
d["stages"] = stages
def lockfile_schema(data: _T) -> _T:
schema = get_lockfile_schema(data)
return schema(data)
class Lockfile(FileMixin):
@classmethod
def validate(cls, d, fname=None):
schema = get_lockfile_schema(d)
try:
return schema(d)
except MultipleInvalid as exc:
raise StageFileFormatError(f"'{fname}' format error: {exc}")
SCHEMA = staticmethod(lockfile_schema) # type: ignore[assignment]
def _verify_filename(self):
pass # lockfile path is hardcoded, so no need to verify here
def _load(self):
def _load(self, **kwargs: Any):
try:
return super()._load()
return super()._load(**kwargs)
except StageFileDoesNotExistError:
# we still need to account for git-ignored dvc.lock file
# even though it may not exist or have been .dvcignored
self._check_gitignored()
return {}, ""
except StageFileFormatError as exc:
raise LockfileCorruptedError(
f"Lockfile '{self.relpath}' is corrupted."
) from exc
def load(self):
d, _ = self._load()
return d
@property
def latest_version_info(self):
@@ -409,10 +394,7 @@ class Lockfile(FileMixin):
if not self.exists():
return
with open(self.path, encoding="utf-8") as f:
d = parse_yaml_for_update(f.read(), self.path)
self.validate(d, self.path)
d, _ = self._load_yaml(round_trip=True)
version = LOCKFILE_VERSION.from_dict(d)
data = d if version == LOCKFILE_VERSION.V1 else d.get("stages", {})
if stage.name not in data:
+5
View File
@@ -364,3 +364,8 @@ class CircularImportError(DvcException):
f"'{dep}' contains invalid circular import. "
f"DVC repo '{a}' already imports from '{b}'."
)
class PrettyDvcException(DvcException):
def __pretty_exc__(self, **kwargs):
"""Print prettier exception message."""
+2 -15
View File
@@ -6,7 +6,6 @@ from functools import partialmethod
from multiprocessing import cpu_count
from typing import Any, ClassVar, Dict, FrozenSet, Optional
from funcy import cached_property
from tqdm.utils import CallbackIOWrapper
from dvc.exceptions import DvcException
@@ -176,12 +175,6 @@ class BaseFileSystem:
"""
raise NotImplementedError
def ls(self, path_info, detail=False):
raise RemoteActionNotImplemented("ls", self.scheme)
def find(self, path_info, detail=False, prefix=None):
raise RemoteActionNotImplemented("find", self.scheme)
def is_empty(self, path_info):
return False
@@ -223,12 +216,6 @@ class BaseFileSystem:
return False
return hash_.endswith(cls.CHECKSUM_DIR_SUFFIX)
@cached_property
def _local_fs(self):
from dvc.fs import LocalFileSystem
return LocalFileSystem()
def upload(
self,
from_info,
@@ -260,7 +247,7 @@ class BaseFileSystem:
**pbar_args,
)
stack.enter_context(pbar)
callback = pbar.as_callback(self._local_fs, from_info)
callback = pbar.as_callback()
if total:
callback.set_size(total)
@@ -317,7 +304,7 @@ class BaseFileSystem:
**pbar_kwargs,
)
stack.enter_context(pbar)
callback = pbar.as_callback(self, from_info)
callback = pbar.as_callback()
with stack:
if download_dir:
+22 -19
View File
@@ -58,9 +58,9 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
out.get_dir_cache(remote=remote)
if out.obj is None:
raise FileNotFoundError
obj = out.obj.trie.get(key)
if obj:
return obj.hash_info
(_, oid) = out.obj.trie.get(key) or (None, None)
if oid:
return oid
raise FileNotFoundError
def _get_fs_path(self, path: PathInfo, remote=None):
@@ -154,7 +154,7 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
self._fetch_dir(out, **kwargs)
base = out.path_info.parts
for key, _ in out.obj: # noqa: B301
for key, _, _ in out.obj: # noqa: B301
trie[base + key] = None
def _walk(self, root, trie, topdown=True, **kwargs):
@@ -166,16 +166,19 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
self._add_dir(trie, out, **kwargs)
root_len = len(root.parts)
for key, out in trie.iteritems(prefix=root.parts): # noqa: B301
if key == root.parts:
continue
try:
for key, out in trie.iteritems(prefix=root.parts): # noqa: B301
if key == root.parts:
continue
name = key[root_len]
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
dirs.add(name)
continue
name = key[root_len]
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
dirs.add(name)
continue
files.append(name)
files.append(name)
except KeyError:
pass
assert topdown
dirs = list(dirs)
@@ -244,16 +247,16 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
meta = self.metadata(path_info)
ret = {"type": "directory" if meta.isdir else "file"}
if meta.is_output and len(meta.outs) == 1 and meta.outs[0].hash_info:
hash_info = meta.outs[0].hash_info
ret["size"] = hash_info.size
ret[hash_info.name] = hash_info.value
out = meta.outs[0]
ret["size"] = out.meta.size
ret[out.hash_info.name] = out.hash_info.value
elif meta.part_of_output:
(out,) = meta.outs
key = path_info.relative_to(out.path_info).parts
obj = out.obj.trie.get(key)
if obj:
ret["size"] = obj.size
ret[obj.hash_info.name] = obj.hash_info.value
(obj_meta, oid) = out.obj.trie.get(key) or (None, None)
if oid:
ret["size"] = obj_meta.size if obj_meta else 0
ret[oid.name] = oid.value
return ret
+121 -45
View File
@@ -1,14 +1,37 @@
import os
import shutil
from functools import lru_cache
from typing import (
IO,
TYPE_CHECKING,
Any,
Dict,
Iterator,
Optional,
Union,
overload,
)
from funcy import cached_property
from tqdm.utils import CallbackIOWrapper
from dvc.path_info import URLInfo
from dvc.progress import DEFAULT_CALLBACK
from .base import BaseFileSystem
from .local import LocalFileSystem
FSPath = URLInfo
AnyFSPath = Union[FSPath, str]
if TYPE_CHECKING:
from typing import Iterable
from typing_extensions import Literal
# An info() entry, might evolve to a TypedDict
# in the future (e.g for properly type 'size' etc).
Entry = Dict[str, Any]
# pylint: disable=no-member
@@ -20,18 +43,8 @@ class FSSpecWrapper(BaseFileSystem):
self.fs_args = {"skip_instance_cache": True}
self.fs_args.update(self._prepare_credentials(**kwargs))
@cached_property
def fs(self):
raise NotImplementedError
def _with_bucket(self, path):
return str(path)
def _strip_bucket(self, entry):
return entry
@staticmethod
def _get_kwargs_from_urls(urlpath):
def _get_kwargs_from_urls(urlpath: str) -> "Dict[str, Any]":
from fsspec.utils import infer_storage_options
options = infer_storage_options(urlpath)
@@ -39,6 +52,28 @@ class FSSpecWrapper(BaseFileSystem):
options.pop("protocol", None)
return options
@cached_property
def fs(self):
raise NotImplementedError
def _with_bucket(self, path: AnyFSPath) -> str:
return str(path)
def _strip_bucket(self, entry: str) -> str:
return entry
@overload
def _strip_buckets(
self, entries: "Iterable[Entry]", detail: "Literal[True]"
) -> "Iterator[Entry]":
...
@overload
def _strip_buckets(
self, entries: "Iterable[str]", detail: "Literal[False]" = False
) -> "Iterator[str]":
...
def _strip_buckets(self, entries, detail=False):
for entry in entries:
if detail:
@@ -48,59 +83,91 @@ class FSSpecWrapper(BaseFileSystem):
entry = self._strip_bucket(entry)
yield entry
def _entry_hook(self, entry):
def _entry_hook(self, entry: "Entry") -> "Entry":
"""Simple hook method to be overridden when wanted to process
entries within info() and ls(detail=True) calls"""
return entry
def _prepare_credentials(
self, **config
): # pylint: disable=unused-argument
self, **config: Dict[str, Any] # pylint: disable=unused-argument
) -> Dict[str, Any]:
"""Prepare the arguments for authentication to the
host filesystem"""
return {}
def _isdir(self, path_info):
def _isdir(self, path_info: FSPath) -> bool:
return self.fs.isdir(self._with_bucket(path_info))
def isdir(self, path_info):
def isdir(self, path_info: FSPath) -> bool:
try:
return self._isdir(path_info)
except FileNotFoundError:
return False
def isfile(self, path_info):
def isfile(self, path_info: FSPath) -> bool:
try:
return not self._isdir(path_info)
except FileNotFoundError:
return False
def is_empty(self, path_info):
def is_empty(self, path_info: FSPath) -> bool:
entry = self.info(path_info)
if entry["type"] == "directory":
return not self.fs.ls(self._with_bucket(path_info))
return entry["size"] == 0
def open(
self, path_info, mode="r", **kwargs
): # pylint: disable=arguments-differ
self,
path_info: FSPath,
mode: str = "r",
encoding: Optional[str] = None,
**kwargs,
) -> "IO": # pylint: disable=arguments-differ
return self.fs.open(self._with_bucket(path_info), mode=mode)
def checksum(self, path_info):
def checksum(self, path_info: FSPath) -> str:
return self.fs.checksum(self._with_bucket(path_info))
def copy(self, from_info, to_info):
def copy(self, from_info: FSPath, to_info: FSPath) -> None:
self.makedirs(to_info.parent)
self.fs.copy(self._with_bucket(from_info), self._with_bucket(to_info))
def exists(self, path_info) -> bool:
def exists(self, path_info: FSPath) -> bool:
return self.fs.exists(self._with_bucket(path_info))
@overload
def ls(
self, path_info: FSPath, detail: "Literal[True]"
) -> "Iterator[Entry]":
...
@overload
def ls(self, path_info: FSPath, detail: "Literal[False]") -> Iterator[str]:
...
def ls(self, path_info, detail=False):
path = self._with_bucket(path_info)
files = self.fs.ls(path, detail=detail)
yield from self._strip_buckets(files, detail=detail)
@overload
def find(
self,
path_info: FSPath,
detail: "Literal[True]",
prefix: Optional[bool],
) -> "Iterator[Entry]":
...
@overload
def find(
self,
path_info: FSPath,
detail: "Literal[False]",
prefix: Optional[bool],
) -> Iterator[str]:
...
# pylint: disable=unused-argument
def find(self, path_info, detail=False, prefix=None):
path = self._with_bucket(path_info)
@@ -110,46 +177,56 @@ class FSSpecWrapper(BaseFileSystem):
yield from self._strip_buckets(files, detail=detail)
def walk_files(self, path_info, **kwargs):
def walk_files(self, path_info: FSPath, **kwargs) -> Iterator[FSPath]:
for file in self.find(path_info, **kwargs):
yield path_info.replace(path=file)
def move(self, from_info, to_info):
def move(self, from_info: FSPath, to_info: FSPath) -> None:
self.fs.move(self._with_bucket(from_info), self._with_bucket(to_info))
def remove(self, path_info):
def remove(self, path_info: FSPath) -> None:
self.fs.rm_file(self._with_bucket(path_info))
def info(self, path_info):
def info(self, path_info: FSPath) -> "Entry":
info = self.fs.info(self._with_bucket(path_info)).copy()
info = self._entry_hook(info)
info["name"] = self._strip_bucket(info["name"])
return info
def makedirs(self, path_info, **kwargs):
def makedirs(self, path_info: FSPath, **kwargs) -> None:
self.fs.makedirs(
self._with_bucket(path_info), exist_ok=kwargs.pop("exist_ok", True)
)
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
self,
from_file: FSPath,
to_info: FSPath,
callback: Any = DEFAULT_CALLBACK,
**kwargs,
) -> None:
self.fs.put_file(
from_file, self._with_bucket(to_info), callback=callback, **kwargs
)
self.fs.invalidate_cache(self._with_bucket(to_info.parent))
def get_file(
self, from_info, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
self,
from_info: FSPath,
to_info: FSPath,
callback: Any = DEFAULT_CALLBACK,
**kwargs,
) -> None:
self.fs.get_file(
self._with_bucket(from_info), to_info, callback=callback, **kwargs
)
def upload_fobj(self, fobj, to_info, **kwargs):
def upload_fobj(self, fobj: IO, to_info: FSPath, **kwargs) -> None:
self.makedirs(to_info.parent)
with self.open(to_info, "wb") as fdest:
shutil.copyfileobj(fobj, fdest, length=fdest.blocksize)
shutil.copyfileobj(
fobj, fdest, length=getattr(fdest, "blocksize", None)
)
# pylint: disable=abstract-method
@@ -157,21 +234,21 @@ class ObjectFSWrapper(FSSpecWrapper):
TRAVERSE_PREFIX_LEN = 3
@lru_cache(512)
def _with_bucket(self, path):
def _with_bucket(self, path: AnyFSPath) -> str:
if isinstance(path, self.PATH_CLS):
return f"{path.bucket}/{path.path}"
return path
return str(path)
def _strip_bucket(self, entry):
def _strip_bucket(self, entry: str) -> str:
try:
bucket, path = entry.split("/", 1)
except ValueError:
# If there is no path attached, only returns
# the bucket (top-level).
bucket, path = entry, None
bucket, path = entry, ""
return path or bucket
def makedirs(self, path_info, **kwargs):
def makedirs(self, path_info: FSPath, **kwargs) -> None:
# For object storages make this method a no-op. The original
# fs.makedirs() method will only check if the bucket exists
# and create if it doesn't though we don't want to support
@@ -179,7 +256,7 @@ class ObjectFSWrapper(FSSpecWrapper):
# simply ignore all mkdir()/makedirs() calls.
return None
def _isdir(self, path_info):
def _isdir(self, path_info: FSPath) -> bool:
# Directory in object storages are interpreted differently
# among different fsspec providers, so this logic is a temporary
# measure for us to adapt as of now. It checks whether it is a
@@ -214,7 +291,9 @@ class ObjectFSWrapper(FSSpecWrapper):
if len(files) == 1 and files[0] == path and self.isdir(path_info):
return None
yield from self._strip_buckets(files, detail=detail)
yield from self._strip_buckets( # pylint: disable=not-an-iterable
files, detail=detail
)
# pylint: disable=arguments-differ
@@ -238,9 +317,6 @@ class NoDirectoriesMixin:
raise NotImplementedError
_LOCAL_FS = LocalFileSystem()
class CallbackMixin:
"""Provides callback support for the filesystem that don't support yet."""
+2 -32
View File
@@ -5,7 +5,7 @@ import re
import threading
from urllib.parse import urlparse
from funcy import cached_property, retry, wrap_prop
from funcy import cached_property, wrap_prop
from dvc.exceptions import DvcException
from dvc.path_info import CloudURLInfo
@@ -34,36 +34,6 @@ class GDriveAuthError(DvcException):
super().__init__(message)
def _gdrive_retry(func):
def should_retry(exc):
from pydrive2.files import ApiRequestError
if not isinstance(exc, ApiRequestError):
return False
error_code = exc.error.get("code", 0)
result = False
if 500 <= error_code < 600:
result = True
if error_code == 403:
result = exc.GetField("reason") in [
"userRateLimitExceeded",
"rateLimitExceeded",
]
if result:
logger.debug(f"Retrying GDrive API call, error: {exc}.")
return result
# 16 tries, start at 0.5s, multiply by golden ratio, cap at 20s
return retry(
16,
timeout=lambda a: min(0.5 * 1.618 ** a, 20),
filter_errors=should_retry,
)(func)
class GDriveURLInfo(CloudURLInfo):
def __init__(self, url):
super().__init__(url)
@@ -219,7 +189,7 @@ class GDriveFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
temporary_save_path = self._gdrive_service_credentials_path
if is_credentials_temp:
with open(temporary_save_path, "w") as cred_file:
with open(temporary_save_path, "w", encoding="utf-8") as cred_file:
cred_file.write(
os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
)
-1
View File
@@ -16,7 +16,6 @@ class GSFileSystem(CallbackMixin, ObjectFSWrapper):
REQUIRES = {"gcsfs": "gcsfs"}
PARAM_CHECKSUM = "etag"
DETAIL_FIELDS = frozenset(("etag", "size"))
TRAVERSE_PREFIX_LEN = 2
def _prepare_credentials(self, **config):
login_info = {"consistency": None}
+60 -252
View File
@@ -1,282 +1,90 @@
import errno
import io
import logging
import os
import re
import shutil
import subprocess
from collections import deque
from contextlib import closing, contextmanager
import threading
from tqdm.utils import CallbackIOWrapper
from funcy import cached_property, wrap_prop
from dvc.hash_info import HashInfo
from dvc.progress import DEFAULT_CALLBACK
from dvc.scheme import Schemes
from dvc.utils import fix_env, tmp_fname
from dvc.utils import fix_env
from .base import BaseFileSystem, RemoteCmdError
from .pool import get_connection
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
logger = logging.getLogger(__name__)
CHECKSUM_REGEX = re.compile(r".*\t.*\t(?P<checksum>.*)")
def _hadoop_fs(cmd, user=None):
cmd = "hadoop fs -" + cmd
if user:
cmd = f"HADOOP_USER_NAME={user} " + cmd
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
# See https://github.com/iterative/dvc/issues/1197.
close_fds = os.name != "nt"
executable = os.getenv("SHELL") if os.name != "nt" else None
p = subprocess.Popen(
cmd,
shell=True,
close_fds=close_fds,
executable=executable,
env=fix_env(os.environ),
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = p.communicate()
if p.returncode != 0:
raise RemoteCmdError("hdfs", cmd, p.returncode, err)
return out.decode("utf-8")
def _group(regex, s, gname):
match = re.match(regex, s)
assert match is not None
return match.group(gname)
def _hadoop_fs_checksum(path_info):
# NOTE: pyarrow doesn't support checksum, so we need to use hadoop
regex = r".*\t.*\t(?P<checksum>.*)"
stdout = _hadoop_fs(f"checksum {path_info.url}", user=path_info.user)
return _group(regex, stdout, "checksum")
class HDFSFileSystem(BaseFileSystem):
# pylint: disable=abstract-method
class HDFSFileSystem(CallbackMixin, FSSpecWrapper):
scheme = Schemes.HDFS
REQUIRES = {"pyarrow": "pyarrow"}
REGEX = r"^hdfs://((?P<user>.*)@)?.*$"
REQUIRES = {"fsspec": "fsspec", "pyarrow": "pyarrow"}
PARAM_CHECKSUM = "checksum"
TRAVERSE_PREFIX_LEN = 2
BLOCK_SIZE = 2 ** 26 # 128MB
def __init__(self, **config):
super().__init__(**config)
self.host = config["host"]
self.user = config.get("user")
self.port = config.get("port")
self.kerb_ticket = config.get("kerb_ticket")
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
return path.path
return super()._with_bucket(path)
@staticmethod
def _get_kwargs_from_urls(urlpath):
from fsspec.implementations.hdfs import PyArrowHDFS
from fsspec.implementations.arrow import HadoopFileSystem
# pylint:disable=protected-access
return PyArrowHDFS._get_kwargs_from_urls(urlpath)
return HadoopFileSystem._get_kwargs_from_urls(urlpath)
def hdfs(self, path_info):
import pyarrow.fs
def _prepare_credentials(self, **config):
return config
# NOTE: HadoopFileSystem is not meant to be closed by us and doesn't
# have close() method or alternative, so we add a noop one to satisfy
# our connection pool.
class HDFSConnection(pyarrow.fs.HadoopFileSystem):
def close(self):
pass
@wrap_prop(threading.Lock())
@cached_property
def fs(self):
from fsspec.implementations.arrow import HadoopFileSystem
return get_connection(
HDFSConnection,
path_info.host,
path_info.port,
user=path_info.user,
kerb_ticket=self.kerb_ticket,
)
@contextmanager
def open(self, path_info, mode="r", encoding=None, **kwargs):
assert mode in {"r", "rt", "rb"}
try:
with self.hdfs(path_info) as hdfs, closing(
hdfs.open_input_stream(path_info.path)
) as fd:
if mode == "rb":
yield fd
else:
yield io.TextIOWrapper(fd, encoding=encoding)
except OSError as e:
# Empty .errno and not specific enough error class in pyarrow,
# see https://issues.apache.org/jira/browse/ARROW-6248
if "file does not exist" in str(e):
raise FileNotFoundError(*e.args)
raise
def exists(self, path_info) -> bool:
assert not isinstance(path_info, list)
assert path_info.scheme == "hdfs"
with self.hdfs(path_info) as hdfs:
import pyarrow.fs
file_info = hdfs.get_file_info(path_info.path)
return file_info.type != pyarrow.fs.FileType.NotFound
def _walk(self, hdfs, root, topdown=True):
import posixpath
from pyarrow.fs import FileSelector, FileType
dirs = deque()
nondirs = deque()
selector = FileSelector(root)
for entry in hdfs.get_file_info(selector):
if entry.type == FileType.Directory:
dirs.append(entry.base_name)
else:
nondirs.append(entry.base_name)
if topdown:
yield root, dirs, nondirs
for dname in dirs:
# NOTE: posixpath.join() is slower
yield from self._walk(
hdfs, f"{root}{posixpath.sep}{dname}", topdown=topdown
)
if not topdown:
yield root, dirs, nondirs
def walk(self, top, topdown=True, onerror=None, **kwargs):
if not self.isdir(top):
return
with self.hdfs(top) as hdfs:
for root, dnames, fnames in self._walk(
hdfs, top.path, topdown=topdown
):
yield top.replace(path=root), dnames, fnames
def walk_files(self, path_info, **kwargs):
for root, _, fnames in self.walk(path_info):
yield from (root / fname for fname in fnames)
def remove(self, path_info):
if path_info.scheme != "hdfs":
raise NotImplementedError
if self.exists(path_info):
logger.debug(f"Removing {path_info.path}")
with self.hdfs(path_info) as hdfs:
import pyarrow.fs
file_info = hdfs.get_file_info(path_info.path)
if file_info.type == pyarrow.fs.FileType.Directory:
hdfs.delete_dir(path_info.path)
else:
hdfs.delete_file(path_info.path)
def makedirs(self, path_info, **kwargs):
with self.hdfs(path_info) as hdfs:
# NOTE: fs.create_dir creates parents by default
hdfs.create_dir(path_info.path)
def copy(self, from_info, to_info, **_kwargs):
# NOTE: hdfs.copy_file is not supported yet in pyarrow
with self.hdfs(to_info) as hdfs:
# NOTE: this is how `hadoop fs -cp` works too: it copies through
# your local machine.
with closing(hdfs.open_input_stream(from_info.path)) as from_fobj:
tmp_info = to_info.parent / tmp_fname(to_info.name)
try:
with closing(
hdfs.open_output_stream(tmp_info.path)
) as tmp_fobj:
shutil.copyfileobj(from_fobj, tmp_fobj)
hdfs.move(tmp_info.path, to_info.path)
except Exception:
self.remove(tmp_info)
raise
def isfile(self, path_info):
with self.hdfs(path_info) as hdfs:
import pyarrow.fs
file_info = hdfs.get_file_info(path_info.path)
return file_info.type == pyarrow.fs.FileType.File
def isdir(self, path_info):
with self.hdfs(path_info) as hdfs:
import pyarrow.fs
file_info = hdfs.get_file_info(path_info.path)
return file_info.type == pyarrow.fs.FileType.Directory
def info(self, path_info):
with self.hdfs(path_info) as hdfs:
from pyarrow.fs import FileType
file_info = hdfs.get_file_info(path_info.path)
if file_info.type is FileType.Directory:
kind = "directory"
elif file_info.type is FileType.File:
kind = "file"
else:
raise FileNotFoundError(
errno.ENOENT, os.strerror(errno.ENOENT), path_info
)
return {"size": file_info.size, "type": kind}
return HadoopFileSystem(**self.fs_args)
def checksum(self, path_info):
return HashInfo(
"checksum",
_hadoop_fs_checksum(path_info),
size=self.getsize(path_info),
return self._checksum(path_info)
def _checksum(self, path_info, **kwargs):
# PyArrow doesn't natively support retrieving the
# checksum, so we have to use hadoop fs
result = self._run_command(
f"checksum {path_info.url}",
env=fix_env(os.environ),
user=path_info.user,
)
if result is None:
return None
def upload_fobj(self, fobj, to_info, **kwargs):
with self.hdfs(to_info) as hdfs:
with hdfs.open_output_stream(to_info.path) as fdest:
shutil.copyfileobj(fobj, fdest, self.BLOCK_SIZE)
match = CHECKSUM_REGEX.match(result)
if match is None:
return None
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
with self.hdfs(to_info) as hdfs:
hdfs.create_dir(to_info.parent.path)
return match.group("checksum")
tmp_file = tmp_fname(to_info.path)
total: int = os.path.getsize(from_file)
callback.set_size(total)
def _run_command(self, cmd, env=None, user=None):
cmd = "hadoop fs -" + cmd
if user:
cmd = f"HADOOP_USER_NAME={user} " + cmd
with open(from_file, "rb") as fobj:
wrapped = CallbackIOWrapper(callback.relative_update, fobj)
with hdfs.open_output_stream(tmp_file) as sobj:
shutil.copyfileobj(wrapped, sobj, self.BLOCK_SIZE)
hdfs.move(tmp_file, to_info.path)
# NOTE: close_fds doesn't work with redirected stdin/stdout/stderr.
# See https://github.com/iterative/dvc/issues/1197.
close_fds = os.name != "nt"
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
with self.hdfs(from_info) as hdfs:
file_info = hdfs.get_file_info(from_info.path)
total = file_info.size
if total:
callback.set_size(total)
executable = os.getenv("SHELL") if os.name != "nt" else None
p = subprocess.Popen(
cmd,
shell=True,
close_fds=close_fds,
executable=executable,
env=env or os.environ,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = p.communicate()
with hdfs.open_input_stream(from_info.path) as sobj:
with open(to_file, "wb+") as fobj:
wrapped = CallbackIOWrapper(callback.relative_update, sobj)
shutil.copyfileobj(wrapped, fobj, self.BLOCK_SIZE)
if p.returncode != 0:
raise subprocess.CalledProcessError(p.returncode, cmd, out, err)
else:
return out.decode("utf-8")
+4
View File
@@ -87,6 +87,9 @@ class HTTPFileSystem(NoDirectoriesMixin, FSSpecWrapper):
ssl=make_context(config["ssl_verify"])
)
# Allow reading proxy configurations from the environment.
client_kwargs["trust_env"] = True
credentials["get_client"] = self.get_client
self.upload_method = config.get("method", "POST")
return credentials
@@ -99,6 +102,7 @@ class HTTPFileSystem(NoDirectoriesMixin, FSSpecWrapper):
attempts=self.SESSION_RETRIES,
factor=self.SESSION_BACKOFF_FACTOR,
max_timeout=self.REQUEST_TIMEOUT,
exceptions=[aiohttp.ClientError],
)
# The default timeout for the aiohttp is 300 seconds
+5 -1
View File
@@ -39,7 +39,11 @@ class LocalFileSystem(BaseFileSystem):
return os.path.lexists(path_info)
def checksum(self, path_info) -> str:
return self.fs.checksum(path_info)
from fsspec.utils import tokenize
st = os.stat(path_info)
return str(int(tokenize([st.st_ino, st.st_mtime, st.st_size]), 16))
def isfile(self, path_info) -> bool:
return os.path.isfile(path_info)
+5
View File
@@ -99,6 +99,11 @@ class BaseS3FileSystem(ObjectFSWrapper):
client["endpoint_url"] = config.get("endpointurl")
client["verify"] = config.get("ssl_verify")
# timeout configuration
config_kwargs = login_info["config_kwargs"]
config_kwargs["read_timeout"] = config.get("read_timeout")
config_kwargs["connect_timeout"] = config.get("connect_timeout")
# encryptions
additional = login_info["s3_additional_kwargs"]
additional["ServerSideEncryption"] = config.get("sse")
+2 -4
View File
@@ -36,10 +36,8 @@ class SSHFileSystem(FSSpecWrapper):
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
path_str = path.path
else:
path_str = super()._with_bucket(path)
return os.path.normpath(path_str)
return path.path
return super()._with_bucket(path)
def _prepare_credentials(self, **config):
self.CAN_TRAVERSE = True
+37 -6
View File
@@ -1,7 +1,9 @@
import errno
import logging
import os
from typing import TYPE_CHECKING
from .base import RemoteActionNotImplemented
from .local import LocalFileSystem
if TYPE_CHECKING:
@@ -12,17 +14,48 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _link(
from_fs: "BaseFileSystem",
from_info: "AnyPath",
to_fs: "BaseFileSystem",
to_info: "DvcPath",
hardlink: bool = False,
) -> None:
if not isinstance(from_fs, type(to_fs)):
raise OSError(errno.EXDEV, "can't link across filesystems")
links = ["reflink"] + ["hardlink"] if hardlink else []
while links:
link = links.pop(0)
try:
func = getattr(to_fs, link)
except AttributeError:
continue
try:
return func(from_info, to_info)
except RemoteActionNotImplemented:
continue
except OSError as exc:
if exc.errno not in [errno.EXDEV, errno.ENOTSUP]:
raise
raise OSError(errno.ENOTSUP, "reflink and hardlink are not supported")
def transfer(
from_fs: "BaseFileSystem",
from_info: "AnyPath",
to_fs: "BaseFileSystem",
to_info: "DvcPath",
move: bool = False,
hardlink: bool = False,
) -> None:
use_move = isinstance(from_fs, type(to_fs)) and move
try:
if use_move:
return to_fs.move(from_info, to_info)
try:
return _link(from_fs, from_info, to_fs, to_info, hardlink=hardlink)
except OSError as exc:
if exc.errno not in [errno.EXDEV, errno.ENOTSUP]:
raise
if isinstance(from_fs, LocalFileSystem):
if not isinstance(from_info, from_fs.PATH_CLS):
@@ -49,8 +82,6 @@ def transfer(
and isinstance(exc.__context__, FileExistsError)
):
logger.debug("'%s' file already exists, skipping", to_info)
if use_move:
from_fs.remove(from_info)
return None
raise
+1 -1
View File
@@ -84,7 +84,7 @@ class WebDAVFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
return path.path[len(self.prefix) :].lstrip("/")
return path
_with_bucket = translate_path_info
_with_bucket = translate_path_info # type: ignore
class WebDAVSFileSystem(WebDAVFileSystem): # pylint:disable=abstract-method
+23 -151
View File
@@ -1,60 +1,24 @@
import logging
import os
import posixpath
import shutil
import threading
from contextlib import contextmanager
from funcy import cached_property, wrap_prop
from dvc.hash_info import HashInfo
from dvc.path_info import CloudURLInfo
from dvc.progress import DEFAULT_CALLBACK
from dvc.scheme import Schemes
from .base import BaseFileSystem
logger = logging.getLogger(__name__)
# pylint:disable=abstract-method
from .fsspec_wrapper import CallbackMixin, FSSpecWrapper
def update_pbar(pbar, total):
"""Update pbar to accept the two arguments passed by hdfs"""
def update(_, bytes_transfered):
if bytes_transfered == -1:
pbar.update_to(total)
return
pbar.update_to(bytes_transfered)
return update
def update_callback(callback, total):
def update(_, bytes_transfered):
if bytes_transfered == -1:
return callback.absolute_update(total)
return callback.relative_update(bytes_transfered)
return update
class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
class WebHDFSFileSystem(CallbackMixin, FSSpecWrapper):
scheme = Schemes.WEBHDFS
PATH_CLS = CloudURLInfo
REQUIRES = {"hdfs": "hdfs"}
REQUIRES = {"fsspec": "fsspec"}
PARAM_CHECKSUM = "checksum"
TRAVERSE_PREFIX_LEN = 2
def __init__(self, **config):
super().__init__(**config)
self.host = config["host"]
self.user = config.get("user")
self.port = config.get("port")
self.hdfscli_config = config.get("hdfscli_config")
self.token = config.get("webhdfs_token")
self.alias = config.get("webhdfs_alias")
def _with_bucket(self, path):
if isinstance(path, self.PATH_CLS):
return f"/{path.path.rstrip('/')}"
return path
@staticmethod
def _get_kwargs_from_urls(urlpath):
@@ -66,115 +30,23 @@ class WebHDFSFileSystem(BaseFileSystem): # pylint:disable=abstract-method
)
)
def _prepare_credentials(self, **config):
self._ssl_verify = config.pop("ssl_verify", True)
principal = config.pop("kerberos_principal", None)
if principal:
config["kerb_kwargs"] = {"principal": principal}
return config
@wrap_prop(threading.Lock())
@cached_property
def hdfs_client(self):
import hdfs
def fs(self):
from fsspec.implementations.webhdfs import WebHDFS
logger.debug("HDFSConfig: %s", self.hdfscli_config)
try:
return hdfs.config.Config(self.hdfscli_config).get_client(
self.alias
)
except hdfs.util.HdfsError as exc:
exc_msg = str(exc)
errors = (
"No alias specified",
"Invalid configuration file",
f"Alias {self.alias} not found",
)
if not any(err in exc_msg for err in errors):
raise
http_url = f"http://{self.host}:{self.port}"
logger.debug("URL: %s", http_url)
if self.token is not None:
client = hdfs.TokenClient(http_url, token=self.token, root="/")
else:
client = hdfs.InsecureClient(
http_url, user=self.user, root="/"
)
return client
@contextmanager
def open(self, path_info, mode="r", encoding=None, **kwargs):
assert mode in {"r", "rt", "rb"}
with self.hdfs_client.read(
path_info.path, encoding=encoding
) as reader:
yield reader
def walk_files(self, path_info, **kwargs):
if not self.exists(path_info):
return
root = path_info.path
for path, _, fnames in self.hdfs_client.walk(root):
for fname in fnames:
yield path_info.replace(path=posixpath.join(path, fname))
def remove(self, path_info):
if path_info.scheme != self.scheme:
raise NotImplementedError
self.hdfs_client.delete(path_info.path)
def exists(self, path_info) -> bool:
assert not isinstance(path_info, list)
assert path_info.scheme == "webhdfs"
status = self.hdfs_client.status(path_info.path, strict=False)
return status is not None
def info(self, path_info):
st = self.hdfs_client.status(path_info.path)
return {"size": st["length"], "type": "file"}
fs = WebHDFS(**self.fs_args)
fs.session.verify = self._ssl_verify
return fs
def checksum(self, path_info):
return HashInfo(
"checksum",
self.hdfs_client.checksum(path_info.path)["bytes"],
size=self.hdfs_client.status(path_info.path)["length"],
)
def copy(self, from_info, to_info, **_kwargs):
with self.hdfs_client.read(from_info.path) as reader:
with self.hdfs_client.write(to_info.path) as writer:
shutil.copyfileobj(reader, writer)
def move(self, from_info, to_info):
self.hdfs_client.makedirs(to_info.parent.path)
self.hdfs_client.rename(from_info.path, to_info.path)
def upload_fobj(self, fobj, to_info, **kwargs):
with self.hdfs_client.write(to_info.path) as fdest:
shutil.copyfileobj(fobj, fdest)
def put_file(
self, from_file, to_info, callback=DEFAULT_CALLBACK, **kwargs
):
total = os.path.getsize(from_file)
callback.set_size(total)
self.hdfs_client.makedirs(to_info.parent.path)
return self.hdfs_client.upload(
to_info.path,
from_file,
overwrite=True,
progress=update_callback(callback, total),
)
def get_file(
self, from_info, to_file, callback=DEFAULT_CALLBACK, **kwargs
):
total = self.getsize(from_info)
if total:
callback.set_size(total)
self.hdfs_client.download(
from_info.path, to_file, progress=update_callback(callback, total)
)
path = self._with_bucket(path_info)
ukey = self.fs.ukey(path)
return ukey["bytes"]
+3 -16
View File
@@ -7,13 +7,8 @@ HASH_DIR_SUFFIX = ".dir"
@dataclass
class HashInfo:
PARAM_SIZE = "size"
PARAM_NFILES = "nfiles"
name: Optional[str]
value: Optional[str]
size: Optional[int] = field(default=None, compare=False)
nfiles: Optional[int] = field(default=None, compare=False)
obj_name: Optional[str] = field(default=None, compare=False)
def __bool__(self):
@@ -27,15 +22,11 @@ class HashInfo:
@classmethod
def from_dict(cls, d):
_d = d.copy() if d else {}
size = _d.pop(cls.PARAM_SIZE, None)
nfiles = _d.pop(cls.PARAM_NFILES, None)
if not _d:
if not d:
return cls(None, None)
((name, value),) = _d.items()
return cls(name, value, size=size, nfiles=nfiles)
((name, value),) = d.items()
return cls(name, value)
def to_dict(self):
ret = OrderedDict()
@@ -43,10 +34,6 @@ class HashInfo:
return ret
ret[self.name] = self.value
if self.size is not None:
ret[self.PARAM_SIZE] = self.size
if self.nfiles is not None:
ret[self.PARAM_NFILES] = self.nfiles
return ret
@property
+2 -3
View File
@@ -11,7 +11,6 @@ from dvc.fs.base import BaseFileSystem
from dvc.path_info import PathInfo
from dvc.pathspec_math import PatternInfo, merge_patterns
from dvc.scheme import Schemes
from dvc.system import System
from dvc.types import AnyPath, List, Optional
from dvc.utils import relpath
from dvc.utils.collections import PathStringTrie
@@ -84,7 +83,7 @@ class DvcIgnorePatterns(DvcIgnore):
else:
return False
if not System.is_unix():
if os.name == "nt":
path = normalize_file(path)
return path
@@ -394,7 +393,7 @@ def init(path):
if os.path.exists(dvcignore):
return dvcignore
with open(dvcignore, "w") as fobj:
with open(dvcignore, "w", encoding="utf-8") as fobj:
fobj.write(
"# Add patterns of files dvc should ignore, which could improve\n"
"# the performance. Learn more at\n"
+18 -14
View File
@@ -1,3 +1,4 @@
import errno
import itertools
import os
import pathlib
@@ -6,19 +7,14 @@ import uuid
import psutil
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc import __version__
from dvc.exceptions import NotDvcRepoError
from dvc.fs import FS_MAP, get_fs_cls, get_fs_config
from dvc.repo import Repo
from dvc.scm.base import SCMError
from dvc.system import System
from dvc.utils import error_link
from dvc.utils.pkg import PKG
from dvc.version import __version__
if PKG is None:
package = ""
else:
package = f"({PKG})"
try:
import importlib.metadata as importlib_metadata
@@ -26,6 +22,9 @@ except ImportError: # < 3.8
import importlib_metadata # type: ignore[no-redef]
package = "" if PKG is None else f"({PKG})"
def get_dvc_info():
info = [
f"DVC version: {__version__} {package}",
@@ -93,7 +92,7 @@ def _get_linktype_support_info(repo):
fname = "." + str(uuid.uuid4())
src = os.path.join(repo.odb.local.cache_dir, fname)
open(src, "w").close()
open(src, "w", encoding="utf-8").close()
dst = os.path.join(repo.root_dir, fname)
cache = []
@@ -105,7 +104,9 @@ def _get_linktype_support_info(repo):
if is_link and not is_link(dst):
status = "broken"
os.unlink(dst)
except DvcException:
except OSError as exc:
if exc.errno != errno.ENOTSUP:
raise
status = "not supported"
if status == "supported":
@@ -136,11 +137,14 @@ def _get_supported_remotes():
def get_fs_type(path):
partition = {
pathlib.Path(part.mountpoint): (part.fstype + " on " + part.device)
for part in psutil.disk_partitions(all=True)
}
partition = {}
for part in psutil.disk_partitions(all=True):
if part.fstype != "":
try:
mountpoint = pathlib.Path(part.mountpoint).resolve()
partition[mountpoint] = part.fstype + " on " + part.device
except PermissionError:
pass
# need to follow the symlink: https://github.com/iterative/dvc/issues/5065
path = pathlib.Path(path).resolve()
+2 -13
View File
@@ -176,25 +176,14 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
except flufl.lock.TimeOutError:
raise LockError(FAILED_TO_LOCK_MESSAGE)
def _set_claimfile(self, pid=None):
super()._set_claimfile(pid)
def _set_claimfile(self):
super()._set_claimfile()
if self._tmp_dir is not None:
# Under Windows file path length is limited so we hash it
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
# Python shutdown [2].
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
# [2] https://github.com/iterative/dvc/issues/2573
def __del__(self):
try:
if self._owned:
self.finalize()
except ImportError:
pass
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
cls = HardlinkLock if hardlink_lock else Lock
+13
View File
@@ -142,9 +142,22 @@ class LoggerHandler(logging.StreamHandler):
super().handleError(record)
raise LoggingException(record)
def emit_pretty_exception(self, exc, verbose: bool = False):
return exc.__pretty_exc__(verbose=verbose)
def emit(self, record):
"""Write to Tqdm's stream so as to not break progress-bars"""
try:
if record.exc_info:
_, exc, *_ = record.exc_info
if hasattr(exc, "__pretty_exc__"):
try:
self.emit_pretty_exception(exc, verbose=_is_verbose())
if not _is_verbose():
return
except Exception: # noqa, pylint: disable=broad-except
pass
msg = self.format(record)
Tqdm.write(
msg, file=self.stream, end=getattr(self, "terminator", "\n")
+16 -1
View File
@@ -2,8 +2,10 @@ import logging
import os
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
Iterator,
Mapping,
Optional,
Tuple,
@@ -181,7 +183,7 @@ class MachineManager:
"""Create and start the specified machine instance."""
config, backend = self.get_config_and_backend(name)
if "startup_script" in config:
with open(config["startup_script"]) as fobj:
with open(config["startup_script"], encoding="utf-8") as fobj:
startup_script = fobj.read()
else:
startup_script = DEFAULT_STARTUP_SCRIPT
@@ -200,3 +202,16 @@ class MachineManager:
def run_shell(self, name: Optional[str]):
config, backend = self.get_config_and_backend(name)
return backend.run_shell(**config)
def status(self, name: str) -> Iterator[Dict[Any, Any]]:
config, backend = self.get_config_and_backend(name)
return backend.instances(**config)
def rename(self, name: str, new: str):
"""move configuration to a new location if the machine is running."""
config, backend = self.get_config_and_backend(name)
return backend.rename(new=new, **config)
def get_executor_kwargs(self, name: Optional[str]):
config, backend = self.get_config_and_backend(name)
return backend.get_executor_kwargs(**config)
+7 -6
View File
@@ -4,13 +4,12 @@ from typing import TYPE_CHECKING, Iterator, Optional
if TYPE_CHECKING:
from dvc.fs.ssh import SSHFileSystem
from dvc.repo.experiments.executor.base import BaseExecutor
from dvc.types import StrPath
class BaseMachineBackend(ABC):
def __init__(self, tmp_dir: "StrPath", **kwargs):
raise NotImplementedError
self.tmp_dir = tmp_dir
@abstractmethod
def create(self, name: Optional[str] = None, **config):
@@ -34,10 +33,8 @@ class BaseMachineBackend(ABC):
"""Spawn an interactive SSH shell for the specified machine."""
@abstractmethod
def get_executor(
self, name: Optional[str] = None, **config
) -> "BaseExecutor":
"""Return an executor instance which can be used for DVC
def get_executor_kwargs(self, name: str, **config) -> dict:
"""Return SSHExecutor kwargs which can be used for DVC
experiment/pipeline execution on the specified machine.
"""
@@ -48,3 +45,7 @@ class BaseMachineBackend(ABC):
) -> Iterator["SSHFileSystem"]:
"""Return an sshfs instance for the default directory on the
specified machine."""
@abstractmethod
def rename(self, name: str, new: str, **config):
"""Rename a machine instance."""
+85 -17
View File
@@ -1,35 +1,103 @@
import os
from contextlib import contextmanager
from functools import partial, partialmethod
from typing import TYPE_CHECKING, Iterator, Optional
from tpi import TerraformProviderIterative, terraform
from dvc.exceptions import DvcException
from dvc.fs.ssh import SSHFileSystem
from dvc.utils.fs import makedirs
from .base import BaseMachineBackend
if TYPE_CHECKING:
from dvc.repo.experiments.executor.base import BaseExecutor
from dvc.types import StrPath
class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
def __init__(self, tmp_dir: "StrPath", **kwargs):
super().__init__(tmp_dir)
@contextmanager
def _sshfs(resource: dict):
from tpi import TerraformProviderIterative
def get_executor(
self, name: Optional[str] = None, **config
) -> "BaseExecutor":
raise NotImplementedError
with TerraformProviderIterative.pemfile(resource) as pem:
fs = SSHFileSystem(
host=resource["instance_ip"],
user="ubuntu",
keyfile=pem,
)
yield fs
class TerraformBackend(BaseMachineBackend):
def __init__(self, tmp_dir: "StrPath", **kwargs):
super().__init__(tmp_dir, **kwargs)
makedirs(tmp_dir, exist_ok=True)
@contextmanager
def make_tpi(self, name: str):
from tpi import TPIError
from tpi.terraform import TerraformBackend as TPIBackend
try:
working_dir = os.path.join(self.tmp_dir, name)
makedirs(working_dir, exist_ok=True)
yield TPIBackend(working_dir=working_dir)
except TPIError as exc:
raise DvcException("TPI operation failed") from exc
def _tpi_func(self, fname, name: Optional[str] = None, **config):
from tpi import TPIError
assert name
with self.make_tpi(name) as tpi:
func = getattr(tpi, fname)
try:
return func(name=name, **config)
except TPIError as exc:
raise DvcException(f"TPI {fname} failed") from exc
create = partialmethod(_tpi_func, "create") # type: ignore[assignment]
destroy = partialmethod(_tpi_func, "destroy") # type: ignore[assignment]
instances = partialmethod(
_tpi_func, "instances"
) # type: ignore[assignment]
run_shell = partialmethod(
_tpi_func, "run_shell"
) # type: ignore[assignment]
def get_executor_kwargs(self, name: str, **config) -> dict:
with self.make_tpi(name) as tpi:
resource = tpi.default_resource(name)
return {
"host": resource["instance_ip"],
"port": SSHFileSystem.DEFAULT_PORT,
"username": "ubuntu",
"fs_factory": partial(_sshfs, dict(resource)),
}
@contextmanager
def get_sshfs( # pylint: disable=unused-argument
self, name: Optional[str] = None, **config
) -> Iterator["SSHFileSystem"]:
resource = self._default_resource(name)
with TerraformProviderIterative.pemfile(resource) as pem:
fs = SSHFileSystem(
host=resource["instance_ip"],
user="ubuntu",
keyfile=pem,
)
assert name
with self.make_tpi(name) as tpi:
resource = tpi.default_resource(name)
with _sshfs(resource) as fs:
yield fs
def rename(self, name: str, new: str, **config):
"""rename a dvc machine instance to a new name"""
import shutil
mtype = "iterative_machine"
new_dir = os.path.join(self.tmp_dir, new)
old_dir = os.path.join(self.tmp_dir, name)
if os.path.exists(new_dir):
raise DvcException(f"rename failed: path {new_dir} already exists")
if not os.path.exists(old_dir):
return
with self.make_tpi(name) as tpi:
tpi.state_mv(f"{mtype}.{name}", f"{mtype}.{new}", **config)
shutil.move(old_dir, new_dir)
+2 -2
View File
@@ -14,8 +14,8 @@ logger = logging.getLogger(__name__)
def check(odb: "ObjectDB", obj: "HashFile", **kwargs):
if isinstance(obj, Tree):
for _, entry in obj:
odb.check(entry.hash_info, **kwargs)
for _, _, oid in obj:
odb.check(oid, **kwargs)
odb.check(obj.hash_info, **kwargs)
+139 -164
View File
@@ -1,4 +1,6 @@
import errno
import logging
from itertools import chain
from shortuuid import uuid
@@ -10,52 +12,18 @@ from dvc.exceptions import (
DvcException,
)
from dvc.ignore import DvcIgnoreFilter
from dvc.objects import check, load
from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
slow_link_guard,
)
from dvc.objects.errors import ObjectFormatError
from dvc.objects.diff import ROOT
from dvc.objects.diff import diff as odiff
from dvc.objects.stage import stage
from dvc.types import Optional
logger = logging.getLogger(__name__)
def _changed(path_info, fs, obj, cache, state=None):
logger.trace("checking if '%s'('%s') has changed.", path_info, obj)
try:
check(cache, obj)
except (FileNotFoundError, ObjectFormatError):
logger.debug(
"cache for '%s'('%s') has changed.", path_info, obj.hash_info
)
return True
actual = state.get(path_info, fs) if state else None
if actual is None:
try:
actual = stage(
cache, path_info, fs, obj.hash_info.name, dry_run=True
)[1].hash_info
except FileNotFoundError:
logger.debug("'%s' doesn't exist.", path_info)
return True
if obj.hash_info != actual:
logger.debug(
"hash value '%s' for '%s' has changed (actual '%s').",
obj.hash_info,
actual,
path_info,
)
return True
logger.trace("'%s' hasn't changed.", path_info)
return False
def _remove(path_info, fs, cache, force=False):
def _remove(path_info, fs, in_cache, force=False):
if not fs.exists(path_info):
return
@@ -63,13 +31,7 @@ def _remove(path_info, fs, cache, force=False):
fs.remove(path_info)
return
current = stage(cache, path_info, fs, fs.PARAM_CHECKSUM, dry_run=True)[
1
].hash_info
try:
obj = load(cache, current)
check(cache, obj)
except (FileNotFoundError, ObjectFormatError):
if not in_cache:
msg = (
f"file/directory '{path_info}' is going to be removed. "
"Are you sure you want to proceed?"
@@ -98,7 +60,7 @@ def _verify_link(cache, path_info, link_type):
def _do_link(cache, from_info, to_info, link_method):
if cache.fs.exists(to_info):
raise DvcException(f"Link '{to_info}' already exists!")
cache.fs.remove(to_info) # broken symlink
link_method(from_info, to_info)
@@ -116,7 +78,9 @@ def _try_links(cache, from_info, to_info, link_types):
_verify_link(cache, to_info, link_types[0])
return
except DvcException as exc:
except OSError as exc:
if exc.errno not in [errno.EXDEV, errno.ENOTSUP]:
raise
logger.debug(
"Cache type '%s' is not supported: %s", link_types[0], exc
)
@@ -130,16 +94,16 @@ def _link(cache, from_info, to_info):
try:
_try_links(cache, from_info, to_info, cache.cache_types)
except FileNotFoundError as exc:
raise CheckoutError([to_info]) from exc
raise CheckoutError([str(to_info)]) from exc
def _cache_is_copy(cache, path_info):
def _confirm_cache_type(cache, path_info):
"""Checks whether cache uses copies."""
if cache.cache_type_confirmed:
return cache.cache_types[0] == "copy"
return
if set(cache.cache_types) <= {"copy"}:
return True
return
workspace_file = path_info.with_name("." + uuid())
test_cache_file = cache.path_info / ".cache_type_test_file"
@@ -154,13 +118,22 @@ def _cache_is_copy(cache, path_info):
cache.fs.remove(test_cache_file)
cache.cache_type_confirmed = True
return cache.cache_types[0] == "copy"
def _relink(cache, cache_info, fs, path_info, in_cache, force):
_remove(path_info, fs, in_cache, force=force)
_link(cache, cache_info, path_info)
# NOTE: Depending on a file system (e.g. on NTFS), `_remove` might reset
# read-only permissions in order to delete a hardlink to protected object,
# which will also reset it for the object itself, making it unprotected,
# so we need to protect it back.
cache.protect(cache_info)
def _checkout_file(
path_info,
fs,
obj,
change,
cache,
force,
progress_callback=None,
@@ -169,24 +142,39 @@ def _checkout_file(
):
"""The file is changed we need to checkout a new copy"""
modified = False
cache_info = cache.hash_to_path_info(obj.hash_info.value)
if fs.exists(path_info):
if not relink and _changed(path_info, fs, obj, cache, state=state):
modified = True
_remove(path_info, fs, cache, force=force)
_link(cache, cache_info, path_info)
else:
if fs.iscopy(path_info) and _cache_is_copy(cache, path_info):
_confirm_cache_type(cache, path_info)
cache_info = cache.hash_to_path_info(change.new.oid.value)
if change.old.oid:
if relink:
if fs.iscopy(path_info) and cache.cache_types[0] == "copy":
cache.unprotect(path_info)
else:
_remove(path_info, fs, cache, force=force)
_link(cache, cache_info, path_info)
_relink(
cache,
cache_info,
fs,
path_info,
change.old.in_cache,
force=force,
)
else:
modified = True
_relink(
cache,
cache_info,
fs,
path_info,
change.old.in_cache,
force=force,
)
else:
_link(cache, cache_info, path_info)
modified = True
if state:
state.save(path_info, fs, obj.hash_info)
state.save(path_info, fs, change.new.oid)
if progress_callback:
progress_callback(str(path_info))
@@ -194,109 +182,83 @@ def _checkout_file(
return modified
def _remove_redundant_files(
path_info, fs, obj, cache, force, dvcignore: Optional[DvcIgnoreFilter]
):
if dvcignore:
existing_files = set(dvcignore.walk_files(fs, path_info))
else:
existing_files = set(fs.walk_files(path_info))
needed_files = {path_info.joinpath(*key) for key, _ in obj}
redundant_files = existing_files - needed_files
for path in redundant_files:
_remove(path, fs, cache, force)
return bool(redundant_files)
def _checkout_dir(
def _diff(
path_info,
fs,
obj,
cache,
force,
progress_callback=None,
relink=False,
dvcignore: Optional[DvcIgnoreFilter] = None,
state=None,
):
modified = False
# Create dir separately so that dir is created
# even if there are no files in it
if not fs.exists(path_info):
modified = True
fs.makedirs(path_info)
logger.debug("Linking directory '%s'.", path_info)
failed = []
for entry_key, entry_obj in obj:
try:
entry_modified = _checkout_file(
path_info.joinpath(*entry_key),
fs,
entry_obj,
cache,
force,
progress_callback,
relink,
state=None,
)
if entry_modified:
modified = True
except CheckoutError as exc:
failed.extend(exc.target_infos)
modified = (
_remove_redundant_files(
path_info, fs, obj, cache, force, dvcignore=dvcignore
old = None
try:
_, _, old = stage(
cache,
path_info,
fs,
obj.hash_info.name if obj else cache.fs.PARAM_CHECKSUM,
dry_run=True,
dvcignore=dvcignore,
)
or modified
)
except FileNotFoundError:
pass
if failed:
raise CheckoutError(failed)
diff = odiff(old, obj, cache)
if state:
state.save(path_info, fs, obj.hash_info)
if relink:
diff.modified.extend(diff.unchanged)
# relink is not modified, assume it as nochange
return modified and not relink
return diff
def _checkout(
diff,
path_info,
fs,
obj,
cache,
force=False,
progress_callback=None,
relink=False,
dvcignore: Optional[DvcIgnoreFilter] = None,
state=None,
):
if not obj.hash_info.isdir:
ret = _checkout_file(
path_info, fs, obj, cache, force, progress_callback, relink, state
)
else:
ret = _checkout_dir(
path_info,
fs,
obj,
cache,
force,
progress_callback,
relink,
dvcignore=dvcignore,
state=state,
)
if not diff:
return
if state:
state.save_link(path_info, fs)
for change in diff.deleted:
entry_path = (
path_info.joinpath(*change.old.key)
if change.old.key != ROOT
else path_info
)
_remove(entry_path, fs, change.old.in_cache, force=force)
return ret
failed = []
for change in chain(diff.added, diff.modified):
entry_path = (
path_info.joinpath(*change.new.key)
if change.new.key != ROOT
else path_info
)
if change.new.oid.isdir:
fs.makedirs(entry_path)
continue
try:
_checkout_file(
entry_path,
fs,
change,
cache,
force,
progress_callback,
relink,
state=state,
)
except CheckoutError as exc:
failed.extend(exc.target_infos)
if failed:
raise CheckoutError(failed)
def checkout(
@@ -311,41 +273,54 @@ def checkout(
dvcignore: Optional[DvcIgnoreFilter] = None,
state=None,
):
if path_info.scheme not in ["local", cache.fs.scheme]:
raise NotImplementedError
failed = None
skip = False
diff = _diff(
path_info,
fs,
obj,
cache,
relink=relink,
dvcignore=dvcignore,
)
failed = []
if not obj:
if not quiet:
logger.warning(
"No file hash info found for '%s'. It won't be created.",
path_info,
)
_remove(path_info, fs, cache, force=force)
failed = path_info
elif not relink and not _changed(path_info, fs, obj, cache, state=state):
failed.append(str(path_info))
elif not diff:
logger.trace("Data '%s' didn't change.", path_info) # type: ignore
skip = True
if failed or skip:
try:
_checkout(
diff,
path_info,
fs,
cache,
force=force,
progress_callback=progress_callback,
relink=relink,
state=state,
)
except CheckoutError as exc:
failed.extend(exc.target_infos)
if diff and state:
state.save_link(path_info, fs)
if not failed:
state.save(path_info, fs, obj.hash_info)
if failed or not diff:
if progress_callback and obj:
progress_callback(str(path_info), len(obj))
if failed:
raise CheckoutError([failed])
raise CheckoutError(failed)
return
logger.debug("Checking out '%s' with cache '%s'.", path_info, obj)
return _checkout(
path_info,
fs,
obj,
cache,
force,
progress_callback,
relink,
dvcignore=dvcignore,
state=state,
)
return bool(diff) and not relink
+8 -4
View File
@@ -81,13 +81,17 @@ class ObjectDB:
from_info: "AnyPath",
to_info: "DvcPath",
_hash_info: "HashInfo",
move: bool = False,
hardlink: bool = False,
):
from dvc import fs
self.makedirs(to_info.parent)
return fs.utils.transfer(
from_fs, from_info, self.fs, to_info, move=move
from_fs,
from_info,
self.fs,
to_info,
hardlink=hardlink,
)
def add(
@@ -95,7 +99,7 @@ class ObjectDB:
path_info: "AnyPath",
fs: "BaseFileSystem",
hash_info: "HashInfo",
move: bool = True,
hardlink: bool = False,
verify: Optional[bool] = None,
):
if self.read_only:
@@ -110,7 +114,7 @@ class ObjectDB:
pass
cache_info = self.hash_to_path_info(hash_info.value)
self._add_file(fs, path_info, cache_info, hash_info, move=move)
self._add_file(fs, path_info, cache_info, hash_info, hardlink=hardlink)
try:
if verify:
+6 -2
View File
@@ -58,12 +58,16 @@ class ReferenceObjectDB(ObjectDB):
from_info: "AnyPath",
to_info: "DvcPath",
hash_info: "HashInfo",
move: bool = False,
hardlink: bool = False,
):
self.makedirs(to_info.parent)
if hash_info.isdir:
return super()._add_file(
from_fs, from_info, to_info, hash_info, move
from_fs,
from_info,
to_info,
hash_info,
hardlink=hardlink,
)
ref_file = ReferenceHashFile(from_info, from_fs, hash_info)
self._obj_cache[hash_info] = ref_file
+134
View File
@@ -0,0 +1,134 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional, Tuple
if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from .file import HashFile
ADD = "add"
MODIFY = "modify"
DELETE = "delete"
UNCHANGED = "unchanged"
@dataclass
class TreeEntry:
in_cache: bool
key: Tuple[str]
oid: Optional["HashInfo"] = field(default=None)
def __bool__(self):
return bool(self.oid)
def __eq__(self, other):
if not isinstance(other, TreeEntry):
return False
if self.key != other.key:
return False
return self.oid == other.oid
@dataclass
class Change:
old: TreeEntry
new: TreeEntry
@property
def typ(self):
if not self.old and not self.new:
return UNCHANGED
if self.old and not self.new:
return DELETE
if not self.old and self.new:
return ADD
if self.old != self.new:
return MODIFY
return UNCHANGED
def __bool__(self):
return self.typ != UNCHANGED
@dataclass
class DiffResult:
added: List[Change] = field(default_factory=list, compare=True)
modified: List[Change] = field(default_factory=list, compare=True)
deleted: List[Change] = field(default_factory=list, compare=True)
unchanged: List[Change] = field(default_factory=list, compare=True)
def __bool__(self):
return bool(self.added or self.modified or self.deleted)
ROOT = ("",)
def diff(
old: Optional["HashFile"], new: Optional["HashFile"], cache
) -> DiffResult:
from .tree import Tree
if old is None and new is None:
return DiffResult()
def _get_keys(obj):
if not obj:
return []
return [ROOT] + (
[key for key, _, _ in obj] if isinstance(obj, Tree) else []
)
old_keys = set(_get_keys(old))
new_keys = set(_get_keys(new))
def _get_oid(obj, key):
if not obj or key == ROOT:
return obj.hash_info if obj else None
entry_obj = obj.get(cache, key)
return entry_obj.hash_info if entry_obj else None
def _in_cache(oid, cache):
from .errors import ObjectFormatError
if not oid:
return False
try:
cache.check(oid)
return True
except (FileNotFoundError, ObjectFormatError):
return False
ret = DiffResult()
for key in old_keys | new_keys:
old_oid = _get_oid(old, key)
new_oid = _get_oid(new, key)
change = Change(
old=TreeEntry(_in_cache(old_oid, cache), key, old_oid),
new=TreeEntry(_in_cache(new_oid, cache), key, new_oid),
)
if change.typ == ADD:
ret.added.append(change)
elif change.typ == MODIFY:
ret.modified.append(change)
elif change.typ == DELETE:
ret.deleted.append(change)
else:
assert change.typ == UNCHANGED
if not change.new.in_cache and not (
change.new.oid and change.new.oid.isdir
):
ret.modified.append(change)
else:
ret.unchanged.append(change)
return ret
+1 -5
View File
@@ -28,10 +28,6 @@ class HashFile:
self.hash_info = hash_info
self.name = name
@property
def size(self):
return self.hash_info.size
def __len__(self):
return 1
@@ -74,7 +70,7 @@ class HashFile:
def _check_hash(self, odb):
from .stage import get_file_hash
actual = get_file_hash(
_, actual = get_file_hash(
self.path_info, self.fs, self.hash_info.name, odb.state
)
+32
View File
@@ -0,0 +1,32 @@
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class Meta:
PARAM_SIZE = "size"
PARAM_NFILES = "nfiles"
size: Optional[int] = field(default=None)
nfiles: Optional[int] = field(default=None)
@classmethod
def from_dict(cls, d):
if not d:
return cls()
size = d.pop(cls.PARAM_SIZE, None)
nfiles = d.pop(cls.PARAM_NFILES, None)
return cls(size=size, nfiles=nfiles)
def to_dict(self):
ret = OrderedDict()
if self.size is not None:
ret[self.PARAM_SIZE] = self.size
if self.nfiles is not None:
ret[self.PARAM_NFILES] = self.nfiles
return ret
+46 -53
View File
@@ -14,6 +14,7 @@ from dvc.utils import file_md5
from .db.reference import ReferenceObjectDB
from .file import HashFile
from .meta import Meta
if TYPE_CHECKING:
from dvc.fs.base import BaseFileSystem
@@ -38,49 +39,48 @@ def _upload_file(path_info, fs, odb, upload_odb):
upload_odb.fs.upload(stream, tmp_info, desc=path_info.name, total=size)
odb.add(tmp_info, upload_odb.fs, stream.hash_info)
return path_info, odb.get(stream.hash_info)
meta = Meta(size=size)
return path_info, meta, odb.get(stream.hash_info)
def _get_file_hash(path_info, fs, name):
info = fs.info(path_info)
if name in info:
assert not info[name].endswith(".dir")
return HashInfo(name, info[name], size=info["size"])
hash_value = info[name]
elif hasattr(fs, name):
func = getattr(fs, name)
hash_value = func(path_info)
elif name == "md5":
hash_value = file_md5(path_info, fs)
else:
raise NotImplementedError
func = getattr(fs, name, None)
if func:
return func(path_info)
if name == "md5":
return HashInfo(
name, file_md5(path_info, fs), size=fs.getsize(path_info)
)
raise NotImplementedError
meta = Meta(size=info["size"])
hash_info = HashInfo(name, hash_value)
return meta, hash_info
def get_file_hash(path_info, fs, name, state=None):
if state:
hash_info = state.get( # pylint: disable=assignment-from-none
meta, hash_info = state.get( # pylint: disable=assignment-from-none
path_info, fs
)
if hash_info:
return hash_info
return meta, hash_info
hash_info = _get_file_hash(path_info, fs, name)
meta, hash_info = _get_file_hash(path_info, fs, name)
if state:
assert ".dir" not in hash_info.value
state.save(path_info, fs, hash_info)
return hash_info
return meta, hash_info
def _get_file_obj(
path_info, fs, name, odb=None, upload_odb=None, dry_run=False
):
def _stage_file(path_info, fs, name, odb=None, upload_odb=None, dry_run=False):
state = odb.state if odb else None
hash_info = get_file_hash(path_info, fs, name, state=state)
meta, hash_info = get_file_hash(path_info, fs, name, state=state)
if upload_odb and not dry_run:
assert odb and name == "md5"
return _upload_file(path_info, fs, odb, upload_odb)
@@ -88,9 +88,10 @@ def _get_file_obj(
if dry_run:
obj = HashFile(path_info, fs, hash_info)
else:
odb.add(path_info, fs, hash_info, move=False)
odb.add(path_info, fs, hash_info, hardlink=False)
obj = odb.get(hash_info)
return path_info, obj
return path_info, meta, obj
def _build_objects(
@@ -113,7 +114,7 @@ def _build_objects(
) as pbar:
worker = pbar.wrap_fn(
partial(
_get_file_obj,
_stage_file,
fs=fs,
name=name,
**kwargs,
@@ -132,8 +133,9 @@ def _iter_objects(path_info, fs, name, **kwargs):
def _build_tree(path_info, fs, name, **kwargs):
from .tree import Tree
tree_meta = Meta(size=0, nfiles=0)
tree = Tree(None, None, None)
for file_info, obj in _iter_objects(path_info, fs, name, **kwargs):
for file_info, meta, obj in _iter_objects(path_info, fs, name, **kwargs):
if DvcIgnore.DVCIGNORE_FILE == file_info.name:
raise DvcIgnoreInCollectedDirError(file_info.parent)
@@ -145,11 +147,15 @@ def _build_tree(path_info, fs, name, **kwargs):
#
# Yes, this is a BUG, as long as we permit "/" in
# filenames on Windows and "\" on Unix
tree.add(file_info.relative_to(path_info).parts, obj)
return tree
tree.add(file_info.relative_to(path_info).parts, meta, obj.hash_info)
tree_meta.size += meta.size
tree_meta.nfiles += 1
return tree_meta, tree
def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
def _stage_tree(path_info, fs, fs_info, name, odb=None, **kwargs):
from .tree import Tree
value = fs_info.get(name)
@@ -157,25 +163,19 @@ def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
hash_info = HashInfo(name, value)
try:
tree = Tree.load(odb, hash_info)
# NOTE: loaded entries are naive objects with hash_infos but no
# path_info. For staging trees, obj.path_info should be relative
# to the staging src `path_info` and src fs
for key, entry in tree:
entry.fs = fs
entry.path_info = path_info.joinpath(*key)
return tree
return Meta(nfiles=len(tree)), tree
except FileNotFoundError:
pass
tree = _build_tree(path_info, fs, name, odb=odb, **kwargs)
meta, tree = _build_tree(path_info, fs, name, odb=odb, **kwargs)
state = odb.state if odb and odb.state else None
hash_info = None
if state:
hash_info = state.get( # pylint: disable=assignment-from-none
_, hash_info = state.get( # pylint: disable=assignment-from-none
path_info, fs
)
tree.digest(hash_info=hash_info)
odb.add(tree.path_info, tree.fs, tree.hash_info, move=True)
odb.add(tree.path_info, tree.fs, tree.hash_info, hardlink=False)
raw = odb.get(tree.hash_info)
# cleanup unneeded memfs tmpfile and return tree based on the
# ODB fs/path
@@ -183,7 +183,7 @@ def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
tree.fs.remove(tree.path_info)
tree.fs = raw.fs
tree.path_info = raw.path_info
return tree
return meta, tree
_url_cache: Dict[str, str] = {}
@@ -225,7 +225,7 @@ def _load_from_state(odb, staging, path_info, fs, name):
from .tree import Tree
state = odb.state
hash_info = state.get(path_info, fs)
meta, hash_info = state.get(path_info, fs)
if hash_info:
for odb_ in (odb, staging):
if odb_.exists(hash_info):
@@ -233,16 +233,9 @@ def _load_from_state(odb, staging, path_info, fs, name):
obj = load(odb_, hash_info)
check(odb_, obj, check_hash=False)
if isinstance(obj, Tree):
obj.hash_info.nfiles = len(obj)
for key, entry in obj:
entry.fs = fs
entry.path_info = path_info.joinpath(*key)
else:
obj.fs = fs
obj.path_info = path_info
meta.nfiles = len(obj)
assert obj.hash_info.name == name
obj.hash_info.size = hash_info.size
return odb_, obj
return odb_, meta, obj
except (ObjectFormatError, FileNotFoundError):
pass
raise FileNotFoundError
@@ -257,7 +250,7 @@ def _stage_external_tree_info(odb, tree, name):
odb.add(tree.path_info, tree.fs, tree.hash_info)
raw = odb.get(tree.hash_info)
hash_info = get_file_hash(raw.path_info, raw.fs, name, state=odb.state)
_, hash_info = get_file_hash(raw.path_info, raw.fs, name, state=odb.state)
tree.path_info = raw.path_info
tree.fs = raw.fs
tree.hash_info.name = hash_info.name
@@ -275,7 +268,7 @@ def stage(
upload: bool = False,
dry_run: bool = False,
**kwargs,
) -> Tuple["ObjectDB", "HashFile"]:
) -> Tuple["ObjectDB", "Meta", "HashFile"]:
"""Stage (prepare) objects from the given path for addition to an ODB.
Returns at tuple of (staging_odb, object) where addition to the ODB can
@@ -300,7 +293,7 @@ def stage(
pass
if details["type"] == "directory":
obj = _get_tree_obj(
meta, obj = _stage_tree(
path_info,
fs,
details,
@@ -314,7 +307,7 @@ def stage(
if name != "md5":
obj = _stage_external_tree_info(odb, obj, name)
else:
_, obj = _get_file_obj(
_, meta, obj = _stage_file(
path_info,
fs,
name,
@@ -326,4 +319,4 @@ def stage(
if odb and odb.state and obj.hash_info:
odb.state.save(path_info, fs, obj.hash_info)
return staging, obj
return staging, meta, obj
+4 -4
View File
@@ -56,7 +56,7 @@ def _indexed_dir_hashes(odb, index, dir_objs, name, cache_odb):
tree = Tree.load(cache_odb, HashInfo(name, dir_hash))
except FileNotFoundError:
continue
file_hashes = [entry.hash_info.value for _, entry in tree]
file_hashes = [oid.value for _, _, oid in tree]
if dir_hash not in index:
logger.debug(
"Indexing new .dir '%s' with '%s' nested files",
@@ -102,9 +102,9 @@ def status(
tree = None
else:
tree = Tree.load(cache_odb, hash_info)
for _, entry in tree:
assert entry.hash_info and entry.hash_info.value
hash_infos[entry.hash_info.value] = entry.hash_info
for _, _, oid in tree:
assert oid and oid.value
hash_infos[oid.value] = oid
if index:
dir_objs[hash_info.value] = tree
hash_infos[hash_info.value] = hash_info
+8 -4
View File
@@ -74,7 +74,7 @@ def _do_transfer(
dir_obj = find_tree_by_obj_id([cache_odb, src], dir_hash)
assert dir_obj
entry_ids = {entry.hash_info for _, entry in dir_obj}
entry_ids = {oid for _, _, oid in dir_obj}
bound_file_ids = all_file_ids & entry_ids
all_file_ids -= entry_ids
@@ -117,7 +117,7 @@ def _do_transfer(
# index successfully pushed dirs
if dest_index:
for dir_obj in succeeded_dir_objs:
file_hashes = {entry.hash_info.value for _, entry in dir_obj}
file_hashes = {oid.value for _, _, oid in dir_obj}
logger.debug(
"Indexing pushed dir '%s' with '%s' nested files",
dir_obj.hash_info,
@@ -133,7 +133,7 @@ def transfer(
obj_ids: Iterable["HashInfo"],
jobs: Optional[int] = None,
verify: bool = False,
move: bool = False,
hardlink: bool = False,
**kwargs,
) -> int:
"""Transfer (copy) the specified objects from one ODB to another.
@@ -157,7 +157,11 @@ def transfer(
def func(hash_info: "HashInfo") -> None:
obj = src.get(hash_info)
return dest.add(
obj.path_info, obj.fs, obj.hash_info, verify=verify, move=move
obj.path_info,
obj.fs,
obj.hash_info,
verify=verify,
hardlink=hardlink,
)
total = len(status.new)
+26 -33
View File
@@ -12,6 +12,8 @@ from .stage import get_file_hash
if TYPE_CHECKING:
from dvc.hash_info import HashInfo
from .meta import Meta
logger = logging.getLogger(__name__)
@@ -20,7 +22,7 @@ class Tree(HashFile):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._dict: Dict[Tuple[str], "HashFile"] = {}
self._dict: Dict[Tuple[str], Tuple["Meta", "HashInfo"]] = {}
@cached_property
def trie(self):
@@ -28,9 +30,9 @@ class Tree(HashFile):
return Trie(self._dict)
def add(self, key, obj):
def add(self, key: Tuple[str], meta: "Meta", oid: "HashInfo"):
self.__dict__.pop("trie", None)
self._dict[key] = obj
self._dict[key] = (meta, oid)
def digest(self, hash_info: Optional["HashInfo"] = None):
from dvc.fs.memory import MemoryFileSystem
@@ -46,20 +48,17 @@ class Tree(HashFile):
if hash_info:
self.hash_info = hash_info
else:
self.hash_info = get_file_hash(path_info, memfs, "md5")
_, self.hash_info = get_file_hash(path_info, memfs, "md5")
assert self.hash_info.value
self.hash_info.value += ".dir"
try:
self.hash_info.size = sum(obj.size for _, obj in self)
except TypeError:
self.hash_info.size = None
self.hash_info.nfiles = len(self)
def __len__(self):
return len(self._dict)
def __iter__(self):
yield from self._dict.items()
yield from (
(key, value[0], value[1]) for key, value in self._dict.items()
)
def as_dict(self):
return self._dict.copy()
@@ -73,10 +72,10 @@ class Tree(HashFile):
{
# NOTE: not using hash_info.to_dict() because we don't want
# size/nfiles fields at this point.
obj.hash_info.name: obj.hash_info.value,
oid.name: oid.value,
self.PARAM_RELPATH: posixpath.sep.join(parts),
}
for parts, obj in self._dict.items() # noqa: B301
for parts, _, oid in self # noqa: B301
),
key=itemgetter(self.PARAM_RELPATH),
)
@@ -94,8 +93,7 @@ class Tree(HashFile):
relpath = entry.pop(cls.PARAM_RELPATH)
parts = tuple(relpath.split(posixpath.sep))
hash_info = HashInfo.from_dict(entry)
obj = HashFile(None, None, hash_info)
tree.add(parts, obj)
tree.add(parts, None, hash_info)
return tree
@classmethod
@@ -118,8 +116,6 @@ class Tree(HashFile):
tree = cls.from_list(raw)
tree.path_info = obj.path_info
tree.fs = obj.fs
for _, entry_obj in tree:
entry_obj.fs = obj.fs
tree.hash_info = hash_info
return tree
@@ -135,37 +131,37 @@ class Tree(HashFile):
"""
tree = Tree(self.path_info, self.fs, self.hash_info)
try:
for key, obj in self.trie.items(prefix):
tree.add(key, obj)
for key, (meta, oid) in self.trie.items(prefix):
tree.add(key, meta, oid)
except KeyError:
pass
return tree
def get(self, prefix: Tuple[str]) -> Optional[HashFile]:
def get(self, odb, prefix: Tuple[str]) -> Optional[HashFile]:
"""Return object at the specified prefix in this tree.
Returns None if no object exists at the specified prefix.
"""
obj = self._dict.get(prefix)
if obj:
return obj
_, oid = self._dict.get(prefix) or (None, None)
if oid:
return odb.get(oid)
tree = Tree(None, None, None)
depth = len(prefix)
try:
for key, obj in self.trie.items(prefix):
tree.add(key[depth:], obj)
for key, (meta, entry_oid) in self.trie.items(prefix):
tree.add(key[depth:], meta, entry_oid)
except KeyError:
return None
tree.digest()
return tree
def _get_dir_size(odb, tree):
def du(odb, tree):
try:
return sum(
odb.fs.getsize(odb.hash_to_path_info(obj.hash_info.value))
for _, obj in tree
odb.fs.getsize(odb.hash_to_path_info(oid.value))
for _, _, oid in tree
)
except FileNotFoundError:
return None
@@ -226,11 +222,8 @@ def merge(odb, ancestor_info, our_info, their_info):
merged_dict = _merge(ancestor.as_dict(), our.as_dict(), their.as_dict())
merged = Tree(None, None, None)
for key, hi in merged_dict.items():
merged.add(key, hi)
for key, (meta, oid) in merged_dict.items():
merged.add(key, meta, oid)
merged.digest()
odb.add(merged.path_info, merged.fs, merged.hash_info)
hash_info = merged.hash_info
hash_info.size = _get_dir_size(odb, merged)
return hash_info
return merged
+41 -29
View File
@@ -1,7 +1,6 @@
import logging
import os
from collections import defaultdict
from copy import copy
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
from urllib.parse import urlparse
@@ -9,7 +8,6 @@ from funcy import collecting, project
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
from dvc import objects, prompt
from dvc.checkout import checkout
from dvc.exceptions import (
CheckoutError,
CollectCacheError,
@@ -17,6 +15,7 @@ from dvc.exceptions import (
MergeError,
RemoteCacheRequiredError,
)
from dvc.objects.checkout import checkout
from .fs import get_cloud_fs
from .fs.hdfs import HDFSFileSystem
@@ -26,6 +25,7 @@ from .hash_info import HashInfo
from .istextfile import istextfile
from .objects import Tree
from .objects.errors import ObjectFormatError
from .objects.meta import Meta
from .objects.stage import stage as ostage
from .objects.transfer import transfer as otransfer
from .scheme import Schemes
@@ -316,6 +316,7 @@ class Output:
# By resolved path, which contains actual location,
# should be absolute and don't contain remote:// refs.
self.stage = stage
self.meta = Meta.from_dict(info)
self.hash_info = HashInfo.from_dict(info)
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
@@ -415,7 +416,7 @@ class Output:
else:
odb = self.repo.odb.local
name = self.fs.PARAM_CHECKSUM
_, obj = ostage(
_, _, obj = ostage(
odb,
self.path_info,
self.fs,
@@ -537,7 +538,15 @@ class Output:
self.verify_metric()
if not self.use_cache:
self.hash_info = self.get_hash()
_, self.meta, obj = ostage(
self.repo.odb.local,
self.path_info,
self.fs,
self.fs.PARAM_CHECKSUM,
dvcignore=self.dvcignore,
dry_run=True,
)
self.hash_info = obj.hash_info
if not self.IS_DEPENDENCY:
logger.debug(
"Output '%s' doesn't use cache. Skipping saving.", self
@@ -546,11 +555,7 @@ class Output:
assert not self.IS_DEPENDENCY
if not self.changed():
logger.debug("Output '%s' didn't change. Skipping saving.", self)
return
_, self.obj = ostage(
_, self.meta, self.obj = ostage(
self.odb,
self.path_info,
self.fs,
@@ -579,7 +584,7 @@ class Output:
if granular:
obj = self._commit_granular_dir(filter_info)
else:
staging, obj = ostage(
staging, _, obj = ostage(
self.odb,
filter_info or self.path_info,
self.fs,
@@ -591,7 +596,7 @@ class Output:
self.odb,
{obj.hash_info},
shallow=False,
move=True,
hardlink=True,
)
checkout(
filter_info or self.path_info,
@@ -606,7 +611,7 @@ class Output:
def _commit_granular_dir(self, filter_info):
prefix = filter_info.relative_to(self.path_info).parts
staging, save_obj = ostage(
staging, _, save_obj = ostage(
self.odb,
self.path_info,
self.fs,
@@ -614,18 +619,18 @@ class Output:
dvcignore=self.dvcignore,
)
save_obj = save_obj.filter(prefix)
checkout_obj = save_obj.get(prefix)
checkout_obj = save_obj.get(self.odb, prefix)
otransfer(
staging,
self.odb,
{save_obj.hash_info} | {entry.hash_info for _, entry in save_obj},
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
shallow=True,
move=True,
hardlink=True,
)
return checkout_obj
def dumpd(self):
ret = copy(self.hash_info.to_dict())
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
if self.is_in_repo:
path = self.path_info.relpath(self.stage.wdir).as_posix()
@@ -709,7 +714,7 @@ class Output:
if filter_info and filter_info != self.path_info:
prefix = filter_info.relative_to(self.path_info).parts
obj = obj.get(prefix)
obj = obj.get(self.odb, prefix)
return obj
@@ -803,7 +808,7 @@ class Output:
upload = not (update and from_fs.isdir(from_info))
jobs = jobs or min((from_fs.jobs, odb.fs.jobs))
staging, obj = ostage(
staging, self.meta, obj = ostage(
odb,
from_info,
from_fs,
@@ -817,7 +822,7 @@ class Output:
odb,
{obj.hash_info},
jobs=jobs,
move=upload,
hardlink=False,
shallow=False,
)
@@ -832,7 +837,7 @@ class Output:
return 1
if not filter_info or filter_info == self.path_info:
return self.hash_info.nfiles or 0
return self.meta.nfiles or 0
obj = self.get_obj(filter_info=filter_info)
return len(obj) if obj else 0
@@ -946,9 +951,9 @@ class Output:
obj.hash_info.obj_name = name
oids = {obj.hash_info}
if isinstance(obj, Tree):
for key, entry_obj in obj:
entry_obj.hash_info.obj_name = self.fs.sep.join([name, *key])
oids.add(entry_obj.hash_info)
for key, _, oid in obj:
oid.obj_name = self.fs.sep.join([name, *key])
oids.add(oid)
return oids
def get_used_external(
@@ -981,8 +986,8 @@ class Output:
ignored = [
self.fs.PARAM_CHECKSUM,
HashInfo.PARAM_SIZE,
HashInfo.PARAM_NFILES,
Meta.PARAM_SIZE,
Meta.PARAM_NFILES,
]
for opt in ignored:
@@ -1000,7 +1005,7 @@ class Output:
)
def merge(self, ancestor, other):
from dvc.objects.tree import merge
from dvc.objects.tree import du, merge
assert other
@@ -1013,9 +1018,16 @@ class Output:
self._check_can_merge(self)
self._check_can_merge(other)
self.hash_info = merge(
merged = merge(
self.odb, ancestor_info, self.hash_info, other.hash_info
)
self.odb.add(merged.path_info, merged.fs, merged.hash_info)
self.hash_info = merged.hash_info
self.meta = Meta(
size=du(self.odb, merged),
nfiles=len(merged),
)
@property
def fspath(self):
@@ -1040,8 +1052,8 @@ ARTIFACT_SCHEMA = {
Output.PARAM_PLOT: bool,
Output.PARAM_PERSIST: bool,
Output.PARAM_CHECKPOINT: bool,
HashInfo.PARAM_SIZE: int,
HashInfo.PARAM_NFILES: int,
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Output.PARAM_ISEXEC: bool,
}
+7 -13
View File
@@ -136,8 +136,8 @@ class Tqdm(tqdm):
return wrapped
def as_callback(self, fs, path_info):
return FsspecCallback(fs, path_info, self)
def as_callback(self):
return FsspecCallback(self)
def close(self):
self.postfix["info"] = ""
@@ -167,21 +167,15 @@ class Tqdm(tqdm):
class FsspecCallback(fsspec.Callback):
def __init__(self, fs, path_info, progress_bar):
self.fs = fs
self.path_info = path_info
def __init__(self, progress_bar):
self.progress_bar = progress_bar
super().__init__()
def set_size(self, size):
"""``set_size()`` is an API that might be called with ``None`` if the
information is not already present on the caller. In that case, we'll
retrieve the size from an ``info()`` call."""
if size is None:
size = self.fs.info(self.path_info)["size"]
self.progress_bar.total = size
self.progress_bar.refresh()
super().set_size(size)
if size is not None:
self.progress_bar.total = size
self.progress_bar.refresh()
super().set_size(size)
def relative_update(self, inc=1):
self.progress_bar.update(inc)
+2 -2
View File
@@ -89,7 +89,7 @@ def write(
page_html = None
if template_path:
with open(template_path) as fobj:
with open(template_path, encoding="utf-8") as fobj:
page_html = fobj.read()
document = HTML(page_html, refresh_seconds=refresh_seconds)
@@ -102,6 +102,6 @@ def write(
index = Path(os.path.join(path, "index.html"))
with open(index, "w") as fd:
with open(index, "w", encoding="utf-8") as fd:
fd.write(document.embed())
return index
+1 -2
View File
@@ -30,7 +30,6 @@ class ImageRenderer(Renderer):
img_path = os.path.join(
static, f"{revision}_{filename.replace(os.sep, '_')}"
)
rel_img_path = relpath(img_path, page_dir_path)
with open(img_path, "wb") as fd:
fd.write(image_data)
return """
@@ -38,7 +37,7 @@ class ImageRenderer(Renderer):
<p>{title}</p>
<img src="{src}">
</div>""".format(
title=revision, src=rel_img_path
title=revision, src=(relpath(img_path, page_dir_path))
)
def _convert(self, path: "StrPath"):
+1 -1
View File
@@ -1,7 +1,7 @@
import os.path
from typing import Dict, List
import dpath
import dpath.util
def get_files(data: Dict) -> List:
+2 -2
View File
@@ -28,7 +28,7 @@ class PlotDataStructureError(DvcException):
def __init__(self):
super().__init__(
"Plot data extraction failed. Please see "
"https://man.dvc.org/plot for supported data formats."
"https://man.dvc.org/plots for supported data formats."
)
@@ -147,7 +147,7 @@ class VegaRenderer(Renderer):
def get_vega(self) -> Optional[str]:
props = self._squash_props()
template = self.templates.load(props.get("template") or "default")
template = self.templates.load(props.get("template", None))
if not props.get("x") and template.has_anchor("x"):
props["append_index"] = True
+45 -13
View File
@@ -120,6 +120,34 @@ class Repo:
return root_dir, dvc_dir, tmp_dir
def _get_database_dir(self, db_name):
# NOTE: by default, store SQLite-based remote indexes and state's
# `links` and `md5s` caches in the repository itself to avoid any
# possible state corruption in 'shared cache dir' scenario, but allow
# user to override this through config when, say, the repository is
# located on a mounted volume — see
# https://github.com/iterative/dvc/issues/4420
base_db_dir = self.config.get(db_name, {}).get("dir", None)
if not base_db_dir:
return self.tmp_dir
import hashlib
from dvc.utils.fs import makedirs
root_dir_hash = hashlib.sha224(
self.root_dir.encode("utf-8")
).hexdigest()
db_dir = os.path.join(
base_db_dir,
self.DVC_DIR,
f"{os.path.basename(self.root_dir)}-{root_dir_hash[0:7]}",
)
makedirs(db_dir, exist_ok=True)
return db_dir
def __init__(
self,
root_dir=None,
@@ -135,7 +163,6 @@ class Repo:
from dvc.data_cloud import DataCloud
from dvc.fs.local import LocalFileSystem
from dvc.lock import LockNoop, make_lock
from dvc.machine import MachineManager
from dvc.objects.db import ODBManager
from dvc.repo.live import Live
from dvc.repo.metrics import Metrics
@@ -183,10 +210,8 @@ class Repo:
friendly=True,
)
# NOTE: storing state and link_state in the repository itself to
# avoid any possible state corruption in 'shared cache dir'
# scenario.
self.state = State(self.root_dir, self.tmp_dir, self.dvcignore)
state_db_dir = self._get_database_dir("state")
self.state = State(self.root_dir, state_db_dir, self.dvcignore)
self.odb = ODBManager(self)
self.stage_cache = StageCache(self)
@@ -198,14 +223,6 @@ class Repo:
self.params = Params(self)
self.live = Live(self)
if self.tmp_dir and (
self.config["feature"].get("machine", False)
or env2bool("DVC_TEST")
):
self.machine = MachineManager(self)
else:
self.machine = None
self.stage_collection_error_handler: Optional[
Callable[[str, Exception], None]
] = None
@@ -272,6 +289,17 @@ class Repo:
return Experiments(self)
@cached_property
def machine(self):
from dvc.machine import MachineManager
if self.tmp_dir and (
self.config["feature"].get("machine", False)
or env2bool("DVC_TEST")
):
return MachineManager(self)
return None
@property
def fs(self) -> "BaseFileSystem":
return self._fs
@@ -452,6 +480,10 @@ class Repo:
return RepoFileSystem(self, subrepos=self.subrepos, **self._fs_conf)
@cached_property
def index_db_dir(self):
return self._get_database_dir("index")
@contextmanager
def open_by_relpath(self, path, remote=None, mode="r", encoding=None):
"""Opens a specified resource as a file descriptor"""
+3 -3
View File
@@ -137,7 +137,7 @@ def _output_paths(repo, targets):
)
if on_working_fs:
_, obj = ostage(
_, _, obj = ostage(
repo.odb.local,
output.path_info,
repo.odb.local.fs,
@@ -164,13 +164,13 @@ def _output_paths(repo, targets):
def _dir_output_paths(path_info, obj, targets=None):
for key, entry_obj in obj:
for key, _, oid in obj:
fname = path_info.joinpath(*key)
if targets is None or any(
fname.isin_or_eq(target) for target in targets
):
# pylint: disable=no-member
yield str(fname), entry_obj.hash_info.value
yield str(fname), oid.value
def _filter_missing(repo_fs, paths):
+39 -3
View File
@@ -10,6 +10,7 @@ from typing import Dict, Iterable, Mapping, Optional
from funcy import cached_property, first
from dvc.dependency.param import MissingParamsError
from dvc.env import DVCLIVE_RESUME
from dvc.exceptions import DvcException
from dvc.path_info import PathInfo
@@ -350,9 +351,19 @@ class Experiments:
)
self.scm.add(self.args_file)
def _format_new_params_msg(self, new_params, config_path):
"""Format an error message for when new parameters are identified"""
new_param_count = len(new_params)
pluralise = "s are" if new_param_count > 1 else " is"
param_list = ", ".join(new_params)
return (
f"{new_param_count} parameter{pluralise} missing "
f"from '{config_path}': {param_list}"
)
def _update_params(self, params: dict):
"""Update experiment params files with the specified values."""
from dvc.utils.collections import merge_params
from dvc.utils.collections import NewParamsFound, merge_params
from dvc.utils.serialize import MODIFIERS
logger.debug("Using experiment params '%s'", params)
@@ -362,7 +373,11 @@ class Experiments:
suffix = path.suffix.lower()
modify_data = MODIFIERS[suffix]
with modify_data(path, fs=self.repo.fs) as data:
merge_params(data, params[params_fname])
try:
merge_params(data, params[params_fname], allow_new=False)
except NewParamsFound as e:
msg = self._format_new_params_msg(e.new_params, path)
raise MissingParamsError(msg)
# Force params file changes to be staged in git
# Otherwise in certain situations the changes to params file may be
@@ -458,6 +473,17 @@ class Experiments:
"\tdvc exp branch <exp> <branch>\n"
)
def _validate_new_ref(self, exp_ref: ExpRefInfo):
from .utils import check_ref_format
if not exp_ref.name:
return
check_ref_format(self.scm, exp_ref)
if self.scm.get_ref(str(exp_ref)):
raise ExperimentExistsError(exp_ref.name)
@scm_locked
def new(self, *args, checkpoint_resume: Optional[str] = None, **kwargs):
"""Create a new experiment.
@@ -470,6 +496,16 @@ class Experiments:
*args, resume_rev=checkpoint_resume, **kwargs
)
name = kwargs.get("name", None)
baseline_sha = kwargs.get("baseline_rev") or self.repo.scm.get_rev()
exp_ref = ExpRefInfo(baseline_sha=baseline_sha, name=name)
try:
self._validate_new_ref(exp_ref)
except ExperimentExistsError as err:
if not (kwargs.get("force", False) or kwargs.get("reset", False)):
raise err
return self._stash_exp(*args, **kwargs)
def _resume_checkpoint(
@@ -638,8 +674,8 @@ class Experiments:
name=item.name,
branch=item.branch,
tmp_dir=base_tmp_dir,
cache_dir=self.repo.odb.local.cache_dir,
)
executor.init_cache(self.repo, stash_rev)
executors[stash_rev] = executor
for ref in (EXEC_HEAD, EXEC_MERGE, EXEC_BASELINE):
+1 -1
View File
@@ -36,7 +36,7 @@ class BaselineMismatchError(DvcException):
class ExperimentExistsError(DvcException):
def __init__(self, name: str):
msg = (
"Reproduced experiment conflicts with existing experiment "
"Experiment conflicts with existing experiment "
f"'{name}'. To overwrite the existing experiment run:\n\n"
"\tdvc exp run -f ...\n\n"
"To run this experiment with a different name run:\n\n"
+11 -40
View File
@@ -14,8 +14,6 @@ from typing import (
Union,
)
from funcy import cached_property
from dvc.env import DVC_EXP_AUTO_PUSH, DVC_EXP_GIT_REMOTE
from dvc.exceptions import DvcException
from dvc.path_info import PathInfo
@@ -24,8 +22,6 @@ from dvc.repo.experiments.base import (
EXEC_BASELINE,
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
EXPS_NAMESPACE,
EXPS_STASH,
@@ -34,7 +30,6 @@ from dvc.repo.experiments.base import (
ExpRefInfo,
UnchangedExperimentError,
)
from dvc.scm import SCM
from dvc.stage import PipelineStage
from dvc.stage.monitor import CheckpointKilledError
from dvc.stage.serialize import to_lockfile
@@ -117,44 +112,21 @@ class BaseExecutor(ABC):
self._init_git(src, branch)
self.name = name
def _init_git(self, scm: "Git", branch: Optional[str] = None):
"""Init git repo and collect executor refs from the specified SCM."""
from dulwich.repo import Repo as DulwichRepo
DulwichRepo.init(os.fspath(self.root_dir))
cwd = os.getcwd()
os.chdir(self.root_dir)
try:
refspec = f"{EXEC_NAMESPACE}/"
scm.push_refspec(self.git_url, refspec, refspec)
if branch:
scm.push_refspec(self.git_url, branch, branch)
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
elif self.scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
if self.scm.get_ref(EXEC_CHECKPOINT):
self.scm.remove_ref(EXEC_CHECKPOINT)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
head = EXEC_BRANCH if branch else EXEC_HEAD
self.scm.checkout(head, detach=True)
merge_rev = self.scm.get_ref(EXEC_MERGE)
self.scm.merge(merge_rev, squash=True, commit=False)
finally:
os.chdir(cwd)
@cached_property
def scm(self):
return SCM(self.root_dir)
@abstractmethod
def _init_git(self, scm: "Git", branch: Optional[str] = None, **kwargs):
"""Init git repo and populate it using exp refs from the specified
SCM instance.
"""
@property
@abstractmethod
def git_url(self) -> str:
pass
@abstractmethod
def init_cache(self, dvc: "Repo", rev: str, run_cache: bool = True):
"""Initialize DVC (cache)."""
@property
def dvc_dir(self) -> str:
return os.path.join(self.root_dir, self._dvc_dir)
@@ -168,8 +140,7 @@ class BaseExecutor(ABC):
return dict_sha256(exp_data)
def cleanup(self):
self.scm.close()
del self.scm
pass
# TODO: come up with better way to stash repro arguments
@staticmethod
@@ -490,7 +461,7 @@ class BaseExecutor(ABC):
push_cache=push_cache,
run_cache=run_cache,
)
except BaseException as exc:
except BaseException as exc: # pylint: disable=broad-except
logger.warning(
"Something went wrong while auto pushing experiment "
f"to the remote '{git_remote}': {exc}"
+53 -5
View File
@@ -1,10 +1,25 @@
import logging
import os
from tempfile import TemporaryDirectory
from typing import Optional
from typing import TYPE_CHECKING, Optional
from funcy import cached_property
from dvc.repo.experiments.base import (
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
)
from dvc.scm import SCM
from .base import BaseExecutor
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.scm.git import Git
logger = logging.getLogger(__name__)
@@ -36,6 +51,15 @@ class BaseLocalExecutor(BaseExecutor):
root_dir = root_dir.replace(os.sep, "/")
return f"file://{root_dir}"
@cached_property
def scm(self):
return SCM(self.root_dir)
def cleanup(self):
super().cleanup()
self.scm.close()
del self.scm
class TempDirExecutor(BaseLocalExecutor):
"""Temp directory experiment executor."""
@@ -51,22 +75,46 @@ class TempDirExecutor(BaseLocalExecutor):
self,
*args,
tmp_dir: Optional[str] = None,
cache_dir: Optional[str] = None,
**kwargs,
):
self._tmp_dir = ExpTemporaryDirectory(dir=tmp_dir)
kwargs["root_dir"] = self._tmp_dir.name
super().__init__(*args, **kwargs)
if cache_dir:
self._config(cache_dir)
logger.debug("Init temp dir executor in dir '%s'", self._tmp_dir)
def _init_git(self, scm: "Git", branch: Optional[str] = None, **kwargs):
from dulwich.repo import Repo as DulwichRepo
DulwichRepo.init(os.fspath(self.root_dir))
refspec = f"{EXEC_NAMESPACE}/"
scm.push_refspec(self.git_url, refspec, refspec, **kwargs)
if branch:
scm.push_refspec(self.git_url, branch, branch, **kwargs)
self.scm.set_ref(EXEC_BRANCH, branch, symbolic=True)
elif self.scm.get_ref(EXEC_BRANCH):
self.scm.remove_ref(EXEC_BRANCH)
if self.scm.get_ref(EXEC_CHECKPOINT):
self.scm.remove_ref(EXEC_CHECKPOINT)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
head = EXEC_BRANCH if branch else EXEC_HEAD
self.scm.checkout(head, detach=True)
merge_rev = self.scm.get_ref(EXEC_MERGE)
self.scm.merge(merge_rev, squash=True, commit=False)
def _config(self, cache_dir):
local_config = os.path.join(self.dvc_dir, "config.local")
logger.debug("Writing experiments local config '%s'", local_config)
with open(local_config, "w") as fobj:
with open(local_config, "w", encoding="utf-8") as fobj:
fobj.write(f"[cache]\n dir = {cache_dir}")
def init_cache(self, dvc: "Repo", rev: str, run_cache: bool = True):
"""Initialize DVC (cache)."""
self._config(dvc.odb.local.cache_dir)
def cleanup(self):
super().cleanup()
logger.debug("Removing tmpdir '%s'", self._tmp_dir)
+159
View File
@@ -0,0 +1,159 @@
import logging
import posixpath
from contextlib import contextmanager
from typing import TYPE_CHECKING, Callable, Optional
from funcy import first
from dvc.fs.ssh import SSHFileSystem
from dvc.repo.experiments.base import (
EXEC_BRANCH,
EXEC_CHECKPOINT,
EXEC_HEAD,
EXEC_MERGE,
EXEC_NAMESPACE,
)
from .base import BaseExecutor
if TYPE_CHECKING:
from dvc.machine import MachineManager
from dvc.repo import Repo
from dvc.scm.git import Git
logger = logging.getLogger(__name__)
@contextmanager
def _sshfs(fs_factory, **kwargs):
if fs_factory:
with fs_factory() as fs:
yield fs
return
yield SSHFileSystem(**kwargs)
class SSHExecutor(BaseExecutor):
"""SSH experiment executor."""
WARN_UNTRACKED = True
QUIET = True
def __init__(
self,
*args,
host: Optional[str] = None,
port: Optional[int] = None,
username: Optional[str] = None,
fs_factory: Optional[Callable] = None,
**kwargs,
):
assert host
self.host: str = host
self.port = port
self.username = username
self._fs_factory = fs_factory
super().__init__(*args, **kwargs)
logger.debug("Init SSH executor for host '%s'", self.host)
@classmethod
def gen_dirname(cls, name: Optional[str] = None):
from shortuuid import uuid
return "-".join([name or "dvc-exp", "executor", uuid()])
@classmethod
def from_machine(
cls,
manager: "MachineManager",
machine_name: Optional[str],
*args,
**kwargs,
):
kwargs["root_dir"] = cls.gen_dirname(kwargs.get("name"))
kwargs.update(manager.get_executor_kwargs(machine_name))
return cls(*args, **kwargs)
def sshfs(self):
return _sshfs(self._fs_factory, host=self.host, port=self.port)
@property
def git_url(self) -> str:
user = f"{self.username}@" if self.username else ""
port = f":{self.port}" if self.port is not None else ""
path = f"{self.root_dir}" if self.root_dir else ""
if path and not posixpath.isabs(path):
path = f"/~/{path}"
return f"ssh://{user}{self.host}{port}{path}"
@property
def abs_url(self) -> str:
assert self._repo_abspath
user = f"{self.username}@" if self.username else ""
port = f":{self.port}" if self.port is not None else ""
return f"ssh://{user}{self.host}{port}{self._repo_abspath}"
@staticmethod
def _git_client_args(fs):
kwargs = {
"password": fs.fs_args.get("password"),
"key_filename": first(fs.fs_args.get("client_keys", [])),
}
return kwargs
def _init_git(self, scm: "Git", branch: Optional[str] = None, **kwargs):
with self.sshfs() as fs:
fs.makedirs(self.root_dir)
self._ssh_cmd(fs, "git init .")
self._ssh_cmd(fs, "git config user.name dvc-exp")
self._ssh_cmd(
fs, "git config user.email [email protected]"
)
result = self._ssh_cmd(fs, "pwd")
path = result.stdout.strip()
self._repo_abspath = path
# TODO: support multiple client key retries in git backends
# (see https://github.com/iterative/dvc/issues/6508)
kwargs.update(self._git_client_args(fs))
refspec = f"{EXEC_NAMESPACE}/"
scm.push_refspec(self.git_url, refspec, refspec, **kwargs)
if branch:
scm.push_refspec(self.git_url, branch, branch, **kwargs)
self._ssh_cmd(fs, f"git symbolic-ref {EXEC_BRANCH} {branch}")
else:
self._ssh_cmd(
fs, f"git symbolic-ref -d {EXEC_BRANCH}", check=False
)
self._ssh_cmd(
fs, f"git update-ref -d {EXEC_CHECKPOINT}", check=False
)
# checkout EXEC_HEAD and apply EXEC_MERGE on top of it without
# committing
head = EXEC_BRANCH if branch else EXEC_HEAD
self._ssh_cmd(fs, f"git checkout {head}")
merge_rev = scm.get_ref(EXEC_MERGE)
self._ssh_cmd(fs, f"git merge --squash --no-commit {merge_rev}")
def _ssh_cmd(self, sshfs, cmd, chdir=None, **kwargs):
working_dir = chdir or self.root_dir
return sshfs.fs.execute(f"cd {working_dir};{cmd}", **kwargs)
def init_cache(self, dvc: "Repo", rev: str, run_cache: bool = True):
from dvc.objects.db import ODBManager, get_odb
from dvc.repo import Repo
from dvc.repo.push import push
cache_url = posixpath.join(
self.abs_url,
self._dvc_dir,
Repo.DVC_DIR,
ODBManager.CACHE_DIR,
)
with self.sshfs() as fs:
odb = get_odb(fs, fs.PATH_CLS(cache_url), **fs.config)
push(dvc, revs=[rev], run_cache=run_cache, odb=odb)
+293
View File
@@ -0,0 +1,293 @@
import logging
import os
from contextlib import contextmanager
from typing import TYPE_CHECKING, Dict, Iterable, cast
from funcy import compact, lremove, post_processing
from rich.prompt import Confirm, Prompt
from rich.rule import Rule
from rich.syntax import Syntax
from dvc.exceptions import DvcException
from dvc.stage import PipelineStage
from dvc.stage.serialize import to_pipeline_file
from dvc.types import OptStr
from dvc.utils.serialize import dumps_yaml
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.dvcfile import DVCFile
from dvc.stage import Stage
from dvc.ui import ui
PROMPTS = {
"cmd": "[b]Command[/b] to execute",
"code": "Path to a [b]code[/b] file/directory",
"data": "Path to a [b]data[/b] file/directory",
"models": "Path to a [b]model[/b] file/directory",
"params": "Path to a [b]parameters[/b] file",
"metrics": "Path to a [b]metrics[/b] file",
"plots": "Path to a [b]plots[/b] file/directory",
"live": "Path to log [b]dvclive[/b] outputs",
}
class RichInputMixin:
"""Prevents exc message from printing in the same line on Ctrl + D/C."""
@classmethod
def get_input(cls, *args, **kwargs) -> str:
try:
return super().get_input(*args, **kwargs) # type: ignore[misc]
except (KeyboardInterrupt, EOFError):
ui.error_write()
raise
class ConfirmationPrompt(RichInputMixin, Confirm):
def make_prompt(self, default):
prompt = self.prompt.copy()
prompt.end = ""
prompt.append(" [")
yes, no = self.choices
for idx, (val, choice) in enumerate(((True, yes), (False, no))):
if idx:
prompt.append("/")
if val == default:
prompt.append(choice.upper(), "green")
else:
prompt.append(choice)
prompt.append("] ")
return prompt
class RequiredPrompt(RichInputMixin, Prompt):
def process_response(self, value: str):
from rich.prompt import InvalidResponse
ret = super().process_response(value)
if not ret:
raise InvalidResponse(
"[prompt.invalid]Response required. Please try again."
)
return ret
def render_default(self, default):
from rich.text import Text
return Text(f"{default!s}", "green")
class SkippablePrompt(RequiredPrompt):
skip_value: str = "n"
def process_response(self, value: str):
ret = super().process_response(value)
return None if ret == self.skip_value else ret
def make_prompt(self, default):
prompt = self.prompt.copy()
prompt.end = ""
prompt.append(" [")
if (
default is not ...
and self.show_default
and isinstance(default, (str, self.response_type))
):
_default = self.render_default(default)
prompt.append(_default)
prompt.append(", ")
prompt.append(f"{self.skip_value} to omit", style="italic")
prompt.append("]")
prompt.append(self.prompt_suffix)
return prompt
@post_processing(dict)
def _prompts(keys: Iterable[str], defaults: Dict[str, OptStr]):
for key in keys:
if key == "cmd":
prompt_cls = RequiredPrompt
else:
prompt_cls = SkippablePrompt
kwargs = {"default": defaults[key]} if key in defaults else {}
prompt = PROMPTS[key]
value = prompt_cls.ask( # type: ignore[call-overload]
prompt, console=ui.error_console, **kwargs
)
yield key, value
@contextmanager
def _disable_logging(highest_level=logging.CRITICAL):
previous_level = logging.root.manager.disable
logging.disable(highest_level)
try:
yield
finally:
logging.disable(previous_level)
PIPELINE_FILE_LINK = (
"https://dvc.org/doc/user-guide/project-structure/pipelines-files"
)
def init_interactive(
name: str,
defaults: Dict[str, str],
provided: Dict[str, str],
show_tree: bool = False,
live: bool = False,
) -> Dict[str, str]:
command = provided.pop("cmd", None)
primary = lremove(provided.keys(), ["code", "data", "models", "params"])
secondary = lremove(
provided.keys(), ["live"] if live else ["metrics", "plots"]
)
ret: Dict[str, str] = {}
if not (command or primary or secondary):
return ret
message = ui.rich_text.assemble(
"This command will guide you to set up a ",
(name, "bright_blue"),
" stage in ",
("dvc.yaml", "green"),
".",
)
doc_link = ui.rich_text.assemble(
"See ", (PIPELINE_FILE_LINK, "repr.url"), "."
)
ui.error_write(message, doc_link, "", sep="\n", styled=True)
if not command:
ret.update(_prompts(["cmd"], defaults))
ui.write()
else:
ret.update({"cmd": command})
ui.write("Enter the paths for dependencies and outputs of the command.")
workspace = {**defaults, **provided}
if show_tree and workspace:
from rich.tree import Tree
tree = Tree(
"DVC assumes the following workspace structure:",
highlight=True,
)
if not live and "live" not in provided:
workspace.pop("live", None)
for value in sorted(workspace.values()):
tree.add(f"[green]{value}[/green]")
ui.error_write(tree, styled=True)
ui.error_write()
ret.update(_prompts(primary, defaults))
ret.update(_prompts(secondary, defaults))
return compact(ret)
def _check_stage_exists(
dvcfile: "DVCFile", name: str, force: bool = False
) -> None:
if not force and dvcfile.exists() and name in dvcfile.stages:
from dvc.stage.exceptions import DuplicateStageName
hint = "Use '--force' to overwrite."
raise DuplicateStageName(
f"Stage '{name}' already exists in 'dvc.yaml'. {hint}"
)
def init(
repo: "Repo",
name: str = None,
type: str = "default", # pylint: disable=redefined-builtin
defaults: Dict[str, str] = None,
overrides: Dict[str, str] = None,
interactive: bool = False,
force: bool = False,
) -> "Stage":
from dvc.dvcfile import make_dvcfile
dvcfile = make_dvcfile(repo, "dvc.yaml")
name = name or type
_check_stage_exists(dvcfile, name, force=force)
defaults = defaults or {}
overrides = overrides or {}
with_live = type == "live"
if interactive:
defaults = init_interactive(
name,
defaults=defaults,
live=with_live,
provided=overrides,
show_tree=True,
)
else:
if with_live:
# suppress `metrics`/`params` if live is selected, unless
# it is also provided via overrides/cli.
# This makes output to be a checkpoint as well.
defaults.pop("metrics", None)
defaults.pop("params", None)
else:
defaults.pop("live", None) # suppress live otherwise
context: Dict[str, str] = {**defaults, **overrides}
assert "cmd" in context
params_kv = []
params = context.get("params")
if params:
from dvc.utils.serialize import LOADERS
assert isinstance(params, str)
_, ext = os.path.splitext(params)
params_kv = [{params: list(LOADERS[ext](params))}]
checkpoint_out = bool(context.get("live"))
models = context.get("models")
stage = repo.stage.create(
name=name,
cmd=context["cmd"],
deps=compact([context.get("code"), context.get("data")]),
params=params_kv,
metrics_no_cache=compact([context.get("metrics")]),
plots_no_cache=compact([context.get("plots")]),
live=context.get("live"),
force=force,
**{"checkpoints" if checkpoint_out else "outs": compact([models])},
)
if interactive:
ui.write(Rule(style="green"), styled=True)
_yaml = dumps_yaml(to_pipeline_file(cast(PipelineStage, stage)))
syn = Syntax(_yaml, "yaml", theme="ansi_dark")
ui.error_write(syn, styled=True)
if not interactive or ConfirmationPrompt.ask(
"Do you want to add the above contents to dvc.yaml?",
console=ui.error_console,
default=True,
):
scm = repo.scm
with _disable_logging(), scm.track_file_changes(autostage=True):
stage.dump(update_lock=False)
stage.ignore_outs()
if params:
scm.track_file(params)
else:
raise DvcException("Aborting ...")
return stage
+9
View File
@@ -157,3 +157,12 @@ def resolve_exp_ref(
msg.extend([f"\t{info}" for info in exp_ref_list])
raise InvalidArgumentError("\n".join(msg))
return exp_ref_list[0]
def check_ref_format(scm: "Git", ref: ExpRefInfo):
# "/" forbidden, only in dvc exp as we didn't support it for now.
if not scm.check_ref_format(str(ref)) or "/" in ref.name:
raise InvalidArgumentError(
f"Invalid exp name {ref.name}, the exp name must follow rules in "
"https://git-scm.com/docs/git-check-ref-format"
)
+69 -2
View File
@@ -1,2 +1,69 @@
def install(self, use_pre_commit_tool):
self.scm.install(use_pre_commit_tool)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from dvc.repo import Repo
from dvc.scm.git import Git
def pre_commit_install(scm: "Git") -> None:
import os
from dvc.utils.serialize import modify_yaml
config_path = os.path.join(scm.root_dir, ".pre-commit-config.yaml")
with modify_yaml(config_path) as config:
entry = {
"repo": "https://github.com/iterative/dvc",
"rev": "master",
"hooks": [
{
"id": "dvc-pre-commit",
"language_version": "python3",
"stages": ["commit"],
},
{
"id": "dvc-pre-push",
"language_version": "python3",
"stages": ["push"],
},
{
"id": "dvc-post-checkout",
"language_version": "python3",
"stages": ["post-checkout"],
"always_run": True,
},
],
}
config["repos"] = config.get("repos", [])
if entry not in config["repos"]:
config["repos"].append(entry)
def install_hooks(scm: "Git") -> None:
hooks = ["post-checkout", "pre-commit", "pre-push"]
for hook in hooks:
scm.verify_hook(hook)
for hook in hooks:
scm.install_hook(hook, f"exec dvc git-hook {hook} $@")
def install(self: "Repo", use_pre_commit_tool: bool = False) -> None:
"""Adds dvc commands to SCM hooks for the repo.
If use_pre_commit_tool is set and pre-commit is installed it will be used
to install the hooks.
"""
from dvc.scm.git import Git
scm = self.scm
if not isinstance(scm, Git):
return
driver = "dvc git-hook merge-driver --ancestor %O --our %A --their %B "
scm.install_merge_driver("dvc", "DVC merge driver", driver)
if use_pre_commit_tool:
return pre_commit_install(scm)
return install_hooks(scm)
+1 -5
View File
@@ -1,5 +1,4 @@
import logging
import os
from typing import TYPE_CHECKING, List, Optional
from dvc.render.utils import render
@@ -19,10 +18,7 @@ def create_summary(out):
html_path = out.path_info.fspath + "_dvc_plots"
index_path = render(
out.repo, plots, metrics=metrics, path=html_path, refresh_seconds=5
)
logger.info(f"\nfile://{os.path.abspath(index_path)}")
render(out.repo, plots, metrics=metrics, path=html_path, refresh_seconds=5)
def summary_path_info(out: "Output") -> Optional["PathInfo"]:
+30 -26
View File
@@ -3,7 +3,7 @@ import io
import logging
import os
from collections import OrderedDict
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
from typing import TYPE_CHECKING, Callable, Dict, Generator, List, Optional
from funcy import cached_property, first, project
@@ -20,6 +20,7 @@ from dvc.utils.serialize import LOADERS
if TYPE_CHECKING:
from dvc.output import Output
from dvc.repo import Repo
from dvc.types import DvcPath
logger = logging.getLogger(__name__)
@@ -47,10 +48,10 @@ class Plots:
recursive: bool = False,
onerror: Optional[Callable] = None,
props: Optional[Dict] = None,
) -> Dict[str, Dict]:
) -> Generator[Dict, None, None]:
"""Collects all props and data for plots.
Returns a structure like:
Generator yielding a structure like:
{rev: {plots.csv: {
props: {x: ..., "header": ..., ...},
data: "unstructured data (as stored for given extension)",
@@ -59,30 +60,20 @@ class Plots:
from dvc.utils.collections import ensure_list
targets = ensure_list(targets)
data: Dict[str, Dict] = {}
for rev in self.repo.brancher(revs=revs):
# .brancher() adds unwanted workspace
if revs is not None and rev not in revs:
continue
rev = rev or "workspace"
data[rev] = self._collect_from_revision(
revision=rev,
targets=targets,
recursive=recursive,
onerror=onerror,
props=props,
)
errored = errored_revisions(data)
if errored:
from dvc.ui import ui
ui.error_write(
"DVC failed to load some plots for following revisions: "
f"'{', '.join(errored)}'."
)
return data
yield {
rev: self._collect_from_revision(
revision=rev,
targets=targets,
recursive=recursive,
onerror=onerror,
props=props,
)
}
@error_handler
def _collect_from_revision(
@@ -135,9 +126,22 @@ class Plots:
if onerror is None:
onerror = onerror_collect
return self.collect(
data: Dict[str, Dict] = {}
for rev_data in self.collect(
targets, revs, recursive, onerror=onerror, props=props
)
):
data.update(rev_data)
errored = errored_revisions(data)
if errored:
from dvc.ui import ui
ui.error_write(
"DVC failed to load some plots for following revisions: "
f"'{', '.join(errored)}'."
)
return data
def diff(self, *args, **kwargs):
from .diff import diff
@@ -161,7 +165,7 @@ class Plots:
props = props or {}
template = props.get("template")
if template:
self.templates.get_template(template)
self.templates.load(template)
(out,) = self.repo.find_outs_by_path(path)
if not out.plot and unset is not None:
@@ -201,7 +205,7 @@ def _collect_plots(
targets: List[str] = None,
rev: str = None,
recursive: bool = False,
) -> Dict[str, Dict]:
) -> Dict["DvcPath", Dict]:
from dvc.repo.collect import collect
plots, path_infos = collect(
+95 -489
View File
@@ -1,15 +1,23 @@
import json
import os
from typing import Any, Dict, Optional
from typing import TYPE_CHECKING, Iterable, Optional
try:
import importlib_resources
except ImportError:
import importlib.resources as importlib_resources # type: ignore[no-redef]
from funcy import cached_property
from dvc.exceptions import DvcException
if TYPE_CHECKING:
from dvc.types import StrPath
class TemplateNotFoundError(DvcException):
def __init__(self, path):
super().__init__(f"Template '{path}' not found.")
def __init__(self, name):
super().__init__(f"Template '{name}' not found.")
class BadTemplateError(DvcException):
@@ -29,25 +37,9 @@ class Template:
EXTENSION = ".json"
ANCHOR = "<DVC_METRIC_{}>"
DEFAULT_CONTENT: Optional[Dict[str, Any]] = None
DEFAULT_NAME: Optional[str] = None
def __init__(self, content=None, name=None):
if content:
self.content = content
else:
self.content = (
json.dumps(
self.DEFAULT_CONTENT,
indent=self.INDENT,
separators=self.SEPARATORS,
)
+ "\n"
)
self.name = name or self.DEFAULT_NAME
assert self.content and self.name
self.filename = self.name + self.EXTENSION
def __init__(self, content, name):
self.content = content
self.name = name
def render(self, data, props=None):
props = props or {}
@@ -55,7 +47,7 @@ class Template:
if self._anchor_str("data") not in self.content:
anchor = self.anchor("data")
raise BadTemplateError(
f"Template '{self.filename}' is not using '{anchor}' anchor"
f"Template '{self.name}' is not using '{anchor}' anchor"
)
if props.get("x"):
@@ -106,466 +98,86 @@ class Template:
raise NoFieldInDataError(field)
class DefaultTemplate(Template):
DEFAULT_NAME = "default"
DEFAULT_CONTENT = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"values": Template.anchor("data")},
"title": Template.anchor("title"),
"width": 300,
"height": 300,
"mark": {"type": "line"},
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
"title": Template.anchor("x_label"),
},
"y": {
"field": Template.anchor("y"),
"type": "quantitative",
"title": Template.anchor("y_label"),
"scale": {"zero": False},
},
"color": {"field": "rev", "type": "nominal"},
},
}
class ConfusionTemplate(Template):
DEFAULT_NAME = "confusion"
DEFAULT_CONTENT = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"values": Template.anchor("data")},
"title": Template.anchor("title"),
"facet": {"field": "rev", "type": "nominal"},
"spec": {
"transform": [
{
"aggregate": [{"op": "count", "as": "xy_count"}],
"groupby": [Template.anchor("y"), Template.anchor("x")],
},
{
"impute": "xy_count",
"groupby": ["rev", Template.anchor("y")],
"key": Template.anchor("x"),
"value": 0,
},
{
"impute": "xy_count",
"groupby": ["rev", Template.anchor("x")],
"key": Template.anchor("y"),
"value": 0,
},
{
"joinaggregate": [
{"op": "max", "field": "xy_count", "as": "max_count"}
],
"groupby": [],
},
{
"calculate": "datum.xy_count / datum.max_count",
"as": "percent_of_max",
},
],
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "nominal",
"sort": "ascending",
"title": Template.anchor("x_label"),
},
"y": {
"field": Template.anchor("y"),
"type": "nominal",
"sort": "ascending",
"title": Template.anchor("y_label"),
},
},
"layer": [
{
"mark": "rect",
"width": 300,
"height": 300,
"encoding": {
"color": {
"field": "xy_count",
"type": "quantitative",
"title": "",
"scale": {"domainMin": 0, "nice": True},
}
},
},
{
"mark": "text",
"encoding": {
"text": {"field": "xy_count", "type": "quantitative"},
"color": {
"condition": {
"test": "datum.percent_of_max > 0.5",
"value": "white",
},
"value": "black",
},
},
},
],
},
}
class NormalizedConfusionTemplate(Template):
DEFAULT_NAME = "confusion_normalized"
DEFAULT_CONTENT = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"values": Template.anchor("data")},
"title": Template.anchor("title"),
"facet": {"field": "rev", "type": "nominal"},
"spec": {
"transform": [
{
"aggregate": [{"op": "count", "as": "xy_count"}],
"groupby": [Template.anchor("y"), Template.anchor("x")],
},
{
"impute": "xy_count",
"groupby": ["rev", Template.anchor("y")],
"key": Template.anchor("x"),
"value": 0,
},
{
"impute": "xy_count",
"groupby": ["rev", Template.anchor("x")],
"key": Template.anchor("y"),
"value": 0,
},
{
"joinaggregate": [
{"op": "sum", "field": "xy_count", "as": "sum_y"}
],
"groupby": [Template.anchor("y")],
},
{
"calculate": "datum.xy_count / datum.sum_y",
"as": "percent_of_y",
},
],
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "nominal",
"sort": "ascending",
"title": Template.anchor("x_label"),
},
"y": {
"field": Template.anchor("y"),
"type": "nominal",
"sort": "ascending",
"title": Template.anchor("y_label"),
},
},
"layer": [
{
"mark": "rect",
"width": 300,
"height": 300,
"encoding": {
"color": {
"field": "percent_of_y",
"type": "quantitative",
"title": "",
"scale": {"domain": [0, 1]},
}
},
},
{
"mark": "text",
"encoding": {
"text": {
"field": "percent_of_y",
"type": "quantitative",
"format": ".2f",
},
"color": {
"condition": {
"test": "datum.percent_of_y > 0.5",
"value": "white",
},
"value": "black",
},
},
},
],
},
}
class ScatterTemplate(Template):
DEFAULT_NAME = "scatter"
DEFAULT_CONTENT = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"values": Template.anchor("data")},
"title": Template.anchor("title"),
"width": 300,
"height": 300,
"layer": [
{
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
"title": Template.anchor("x_label"),
},
"y": {
"field": Template.anchor("y"),
"type": "quantitative",
"title": Template.anchor("y_label"),
"scale": {"zero": False},
},
"color": {"field": "rev", "type": "nominal"},
},
"layer": [
{"mark": "point"},
{
"selection": {
"label": {
"type": "single",
"nearest": True,
"on": "mouseover",
"encodings": ["x"],
"empty": "none",
"clear": "mouseout",
}
},
"mark": "point",
"encoding": {
"opacity": {
"condition": {
"selection": "label",
"value": 1,
},
"value": 0,
}
},
},
],
},
{
"transform": [{"filter": {"selection": "label"}}],
"layer": [
{
"encoding": {
"text": {
"type": "quantitative",
"field": Template.anchor("y"),
},
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
},
"y": {
"field": Template.anchor("y"),
"type": "quantitative",
},
},
"layer": [
{
"mark": {
"type": "text",
"align": "left",
"dx": 5,
"dy": -5,
},
"encoding": {
"color": {
"type": "nominal",
"field": "rev",
}
},
}
],
}
],
},
],
}
class SmoothLinearTemplate(Template):
DEFAULT_NAME = "smooth"
DEFAULT_CONTENT = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"values": Template.anchor("data")},
"title": Template.anchor("title"),
"mark": {"type": "line"},
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
"title": Template.anchor("x_label"),
},
"y": {
"field": Template.anchor("y"),
"type": "quantitative",
"title": Template.anchor("y_label"),
"scale": {"zero": False},
},
"color": {"field": "rev", "type": "nominal"},
},
"transform": [
{
"loess": Template.anchor("y"),
"on": Template.anchor("x"),
"groupby": ["rev"],
"bandwidth": 0.3,
}
],
}
class LinearTemplate(Template):
DEFAULT_NAME = "linear"
DEFAULT_CONTENT = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"data": {"values": Template.anchor("data")},
"title": Template.anchor("title"),
"width": 300,
"height": 300,
"layer": [
{
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
"title": Template.anchor("x_label"),
},
"y": {
"field": Template.anchor("y"),
"type": "quantitative",
"title": Template.anchor("y_label"),
"scale": {"zero": False},
},
"color": {"field": "rev", "type": "nominal"},
},
"layer": [
{"mark": "line"},
{
"selection": {
"label": {
"type": "single",
"nearest": True,
"on": "mouseover",
"encodings": ["x"],
"empty": "none",
"clear": "mouseout",
}
},
"mark": "point",
"encoding": {
"opacity": {
"condition": {
"selection": "label",
"value": 1,
},
"value": 0,
}
},
},
],
},
{
"transform": [{"filter": {"selection": "label"}}],
"layer": [
{
"mark": {"type": "rule", "color": "gray"},
"encoding": {
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
}
},
},
{
"encoding": {
"text": {
"type": "quantitative",
"field": Template.anchor("y"),
},
"x": {
"field": Template.anchor("x"),
"type": "quantitative",
},
"y": {
"field": Template.anchor("y"),
"type": "quantitative",
},
},
"layer": [
{
"mark": {
"type": "text",
"align": "left",
"dx": 5,
"dy": -5,
},
"encoding": {
"color": {
"type": "nominal",
"field": "rev",
}
},
}
],
},
],
},
],
}
class PlotTemplates:
TEMPLATES_DIR = "plots"
TEMPLATES = [
DefaultTemplate,
LinearTemplate,
ConfusionTemplate,
NormalizedConfusionTemplate,
ScatterTemplate,
SmoothLinearTemplate,
]
PKG_TEMPLATES_DIR = "templates"
@cached_property
def templates_dir(self):
return os.path.join(self.dvc_dir, self.TEMPLATES_DIR)
def get_template(self, path):
if os.path.exists(path):
return path
@staticmethod
def _find(templates, template_name):
for template in templates:
if template.endswith(template_name) or template.endswith(
template_name + ".json"
):
return template
return None
if self.dvc_dir and os.path.exists(self.dvc_dir):
t_path = os.path.join(self.templates_dir, path)
if os.path.exists(t_path):
return t_path
def _find_in_project(self, name: str) -> Optional["StrPath"]:
full_path = os.path.abspath(name)
if os.path.exists(full_path):
return full_path
all_templates = [
if os.path.exists(self.templates_dir):
templates = [
os.path.join(root, file)
for root, _, files in os.walk(self.templates_dir)
for file in files
]
matches = [
template
for template in all_templates
if os.path.splitext(template)[0] == t_path
]
if matches:
assert len(matches) == 1
return matches[0]
found = self._find(templates, name)
if found:
return os.path.join(self.templates_dir, found)
return None
raise TemplateNotFoundError(path)
@staticmethod
def _get_templates() -> Iterable[str]:
if (
importlib_resources.files(__package__)
.joinpath(PlotTemplates.PKG_TEMPLATES_DIR)
.is_dir()
):
entries = (
importlib_resources.files(__package__)
.joinpath(PlotTemplates.PKG_TEMPLATES_DIR)
.iterdir()
)
return [entry.name for entry in entries]
return []
@staticmethod
def _load_from_pkg(name):
templates = PlotTemplates._get_templates()
found = PlotTemplates._find(templates, name)
if found:
return (
(
importlib_resources.files(__package__)
/ PlotTemplates.PKG_TEMPLATES_DIR
/ found
)
.read_bytes()
.decode("utf-8")
)
return None
def load(self, name: str = None) -> Template:
if name is not None:
template_path = self._find_in_project(name)
if template_path:
with open(template_path, "r", encoding="utf-8") as fd:
content = fd.read()
return Template(content, name)
else:
name = "linear"
content = self._load_from_pkg(name)
if content:
return Template(content, name)
raise TemplateNotFoundError(name)
def __init__(self, dvc_dir):
self.dvc_dir = dvc_dir
@@ -574,24 +186,18 @@ class PlotTemplates:
from dvc.utils.fs import makedirs
makedirs(self.templates_dir, exist_ok=True)
for t in self.TEMPLATES:
self._dump(t())
def _dump(self, template):
path = os.path.join(self.templates_dir, template.filename)
with open(path, "w") as fd:
fd.write(template.content)
def load(self, name):
try:
path = self.get_template(name)
with open(path) as fd:
content = fd.read()
return Template(content, name=name)
except TemplateNotFoundError:
for template in self.TEMPLATES:
if template.DEFAULT_NAME == name:
return template()
raise
templates = self._get_templates()
for template in templates:
content = (
importlib_resources.files(__package__)
.joinpath(PlotTemplates.PKG_TEMPLATES_DIR)
.joinpath(template)
.read_text()
)
with open(
os.path.join(self.templates_dir, template),
"w",
encoding="utf-8",
) as fd:
fd.write(content)
+107
View File
@@ -0,0 +1,107 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": "<DVC_METRIC_DATA>"
},
"title": "<DVC_METRIC_TITLE>",
"facet": {
"field": "rev",
"type": "nominal"
},
"spec": {
"transform": [
{
"aggregate": [
{
"op": "count",
"as": "xy_count"
}
],
"groupby": [
"<DVC_METRIC_Y>",
"<DVC_METRIC_X>"
]
},
{
"impute": "xy_count",
"groupby": [
"rev",
"<DVC_METRIC_Y>"
],
"key": "<DVC_METRIC_X>",
"value": 0
},
{
"impute": "xy_count",
"groupby": [
"rev",
"<DVC_METRIC_X>"
],
"key": "<DVC_METRIC_Y>",
"value": 0
},
{
"joinaggregate": [
{
"op": "max",
"field": "xy_count",
"as": "max_count"
}
],
"groupby": []
},
{
"calculate": "datum.xy_count / datum.max_count",
"as": "percent_of_max"
}
],
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "nominal",
"sort": "ascending",
"title": "<DVC_METRIC_X_LABEL>"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "nominal",
"sort": "ascending",
"title": "<DVC_METRIC_Y_LABEL>"
}
},
"layer": [
{
"mark": "rect",
"width": 300,
"height": 300,
"encoding": {
"color": {
"field": "xy_count",
"type": "quantitative",
"title": "",
"scale": {
"domainMin": 0,
"nice": true
}
}
}
},
{
"mark": "text",
"encoding": {
"text": {
"field": "xy_count",
"type": "quantitative"
},
"color": {
"condition": {
"test": "datum.percent_of_max > 0.5",
"value": "white"
},
"value": "black"
}
}
}
]
}
}
@@ -0,0 +1,112 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": "<DVC_METRIC_DATA>"
},
"title": "<DVC_METRIC_TITLE>",
"facet": {
"field": "rev",
"type": "nominal"
},
"spec": {
"transform": [
{
"aggregate": [
{
"op": "count",
"as": "xy_count"
}
],
"groupby": [
"<DVC_METRIC_Y>",
"<DVC_METRIC_X>"
]
},
{
"impute": "xy_count",
"groupby": [
"rev",
"<DVC_METRIC_Y>"
],
"key": "<DVC_METRIC_X>",
"value": 0
},
{
"impute": "xy_count",
"groupby": [
"rev",
"<DVC_METRIC_X>"
],
"key": "<DVC_METRIC_Y>",
"value": 0
},
{
"joinaggregate": [
{
"op": "sum",
"field": "xy_count",
"as": "sum_y"
}
],
"groupby": [
"<DVC_METRIC_Y>"
]
},
{
"calculate": "datum.xy_count / datum.sum_y",
"as": "percent_of_y"
}
],
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "nominal",
"sort": "ascending",
"title": "<DVC_METRIC_X_LABEL>"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "nominal",
"sort": "ascending",
"title": "<DVC_METRIC_Y_LABEL>"
}
},
"layer": [
{
"mark": "rect",
"width": 300,
"height": 300,
"encoding": {
"color": {
"field": "percent_of_y",
"type": "quantitative",
"title": "",
"scale": {
"domain": [
0,
1
]
}
}
}
},
{
"mark": "text",
"encoding": {
"text": {
"field": "percent_of_y",
"type": "quantitative",
"format": ".2f"
},
"color": {
"condition": {
"test": "datum.percent_of_y > 0.5",
"value": "white"
},
"value": "black"
}
}
}
]
}
}
+116
View File
@@ -0,0 +1,116 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": "<DVC_METRIC_DATA>"
},
"title": "<DVC_METRIC_TITLE>",
"width": 300,
"height": 300,
"layer": [
{
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative",
"title": "<DVC_METRIC_X_LABEL>"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "quantitative",
"title": "<DVC_METRIC_Y_LABEL>",
"scale": {
"zero": false
}
},
"color": {
"field": "rev",
"type": "nominal"
}
},
"layer": [
{
"mark": "line"
},
{
"selection": {
"label": {
"type": "single",
"nearest": true,
"on": "mouseover",
"encodings": [
"x"
],
"empty": "none",
"clear": "mouseout"
}
},
"mark": "point",
"encoding": {
"opacity": {
"condition": {
"selection": "label",
"value": 1
},
"value": 0
}
}
}
]
},
{
"transform": [
{
"filter": {
"selection": "label"
}
}
],
"layer": [
{
"mark": {
"type": "rule",
"color": "gray"
},
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative"
}
}
},
{
"encoding": {
"text": {
"type": "quantitative",
"field": "<DVC_METRIC_Y>"
},
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "quantitative"
}
},
"layer": [
{
"mark": {
"type": "text",
"align": "left",
"dx": 5,
"dy": -5
},
"encoding": {
"color": {
"type": "nominal",
"field": "rev"
}
}
}
]
}
]
}
]
}
+104
View File
@@ -0,0 +1,104 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": "<DVC_METRIC_DATA>"
},
"title": "<DVC_METRIC_TITLE>",
"width": 300,
"height": 300,
"layer": [
{
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative",
"title": "<DVC_METRIC_X_LABEL>"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "quantitative",
"title": "<DVC_METRIC_Y_LABEL>",
"scale": {
"zero": false
}
},
"color": {
"field": "rev",
"type": "nominal"
}
},
"layer": [
{
"mark": "point"
},
{
"selection": {
"label": {
"type": "single",
"nearest": true,
"on": "mouseover",
"encodings": [
"x"
],
"empty": "none",
"clear": "mouseout"
}
},
"mark": "point",
"encoding": {
"opacity": {
"condition": {
"selection": "label",
"value": 1
},
"value": 0
}
}
}
]
},
{
"transform": [
{
"filter": {
"selection": "label"
}
}
],
"layer": [
{
"encoding": {
"text": {
"type": "quantitative",
"field": "<DVC_METRIC_Y>"
},
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "quantitative"
}
},
"layer": [
{
"mark": {
"type": "text",
"align": "left",
"dx": 5,
"dy": -5
},
"encoding": {
"color": {
"type": "nominal",
"field": "rev"
}
}
}
]
}
]
}
]
}
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": "<DVC_METRIC_DATA>"
},
"title": "<DVC_METRIC_TITLE>",
"width": 300,
"height": 300,
"mark": {
"type": "line"
},
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative",
"title": "<DVC_METRIC_X_LABEL>"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "quantitative",
"title": "<DVC_METRIC_Y_LABEL>",
"scale": {
"zero": false
}
},
"color": {
"field": "rev",
"type": "nominal"
}
}
}
+39
View File
@@ -0,0 +1,39 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"values": "<DVC_METRIC_DATA>"
},
"title": "<DVC_METRIC_TITLE>",
"mark": {
"type": "line"
},
"encoding": {
"x": {
"field": "<DVC_METRIC_X>",
"type": "quantitative",
"title": "<DVC_METRIC_X_LABEL>"
},
"y": {
"field": "<DVC_METRIC_Y>",
"type": "quantitative",
"title": "<DVC_METRIC_Y_LABEL>",
"scale": {
"zero": false
}
},
"color": {
"field": "rev",
"type": "nominal"
}
},
"transform": [
{
"loess": "<DVC_METRIC_Y>",
"on": "<DVC_METRIC_X>",
"groupby": [
"rev"
],
"bandwidth": 0.3
}
]
}
+14 -4
View File
@@ -1,8 +1,13 @@
from typing import TYPE_CHECKING, Optional
from dvc.exceptions import FileTransferError, UploadError
from ..utils import glob_targets
from . import locked
if TYPE_CHECKING:
from dvc.objects.db.base import ObjectDB
@locked
def push(
@@ -18,8 +23,11 @@ def push(
run_cache=False,
revs=None,
glob=False,
odb: Optional["ObjectDB"] = None,
):
used_run_cache = self.stage_cache.push(remote) if run_cache else []
used_run_cache = (
self.stage_cache.push(remote, odb=odb) if run_cache else []
)
if isinstance(targets, str):
targets = [targets]
@@ -41,11 +49,13 @@ def push(
)
pushed = len(used_run_cache)
for odb, obj_ids in used.items():
if odb and odb.read_only:
for dest_odb, obj_ids in used.items():
if dest_odb and dest_odb.read_only:
continue
try:
pushed += self.cloud.push(obj_ids, jobs, remote=remote, odb=odb)
pushed += self.cloud.push(
obj_ids, jobs, remote=remote, odb=odb or dest_odb
)
except FileTransferError as exc:
raise UploadError(exc.amount)
return pushed
+2 -2
View File
@@ -35,7 +35,7 @@ class RWLockFileFormatError(DvcException):
def _edit_rwlock(lock_dir):
path = os.path.join(lock_dir, "rwlock")
try:
with open(path) as fobj:
with open(path, encoding="utf-8") as fobj:
lock = SCHEMA(json.load(fobj))
except FileNotFoundError:
lock = SCHEMA({})
@@ -46,7 +46,7 @@ def _edit_rwlock(lock_dir):
lock["read"] = defaultdict(list, lock["read"])
lock["write"] = defaultdict(dict, lock["write"])
yield lock
with open(path, "w+") as fobj:
with open(path, "w+", encoding="utf-8") as fobj:
json.dump(lock, fobj)
+22 -4
View File
@@ -1,7 +1,9 @@
from collections.abc import Mapping
from voluptuous import Any, Optional, Required, Schema
from dvc import dependency, output
from dvc.hash_info import HashInfo
from dvc.objects.meta import Meta
from dvc.output import CHECKSUMS_SCHEMA, Output
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
@@ -24,8 +26,8 @@ SINGLE_STAGE_SCHEMA = {
DATA_SCHEMA = {
**CHECKSUMS_SCHEMA,
Required("path"): str,
HashInfo.PARAM_SIZE: int,
HashInfo.PARAM_NFILES: int,
Meta.PARAM_SIZE: int,
Meta.PARAM_NFILES: int,
Output.PARAM_ISEXEC: bool,
}
LOCK_FILE_STAGE_SCHEMA = {
@@ -94,11 +96,27 @@ STAGE_DEFINITION = {
Optional(StageParams.PARAM_LIVE): Any(str, LIVE_PSTAGE_SCHEMA),
}
def either_or(primary, fallback, fallback_includes=None):
def validator(data):
schema = primary
if (
isinstance(data, Mapping)
and set(fallback_includes or []) & data.keys()
):
schema = fallback
return Schema(schema)(data)
return validator
FOREACH_IN = {
Required(FOREACH_KWD): Any(dict, list, str),
Required(DO_KWD): STAGE_DEFINITION,
}
SINGLE_PIPELINE_STAGE_SCHEMA = {str: Any(FOREACH_IN, STAGE_DEFINITION)}
SINGLE_PIPELINE_STAGE_SCHEMA = {
str: either_or(STAGE_DEFINITION, FOREACH_IN, [FOREACH_KWD, DO_KWD])
}
MULTI_STAGE_SCHEMA = {
STAGES: SINGLE_PIPELINE_STAGE_SCHEMA,
VARS_KWD: VARS_SCHEMA,
+48
View File
@@ -0,0 +1,48 @@
"""DVC re-implementation of fsspec's dedicated async event loop."""
import asyncio
import os
import threading
from typing import List, Optional
from fsspec.asyn import ( # noqa: F401, pylint:disable=unused-import
_selector_policy,
sync,
sync_wrapper,
)
# dedicated async IO thread
iothread: List[Optional[threading.Thread]] = [None]
# global DVC event loop
default_loop: List[Optional[asyncio.AbstractEventLoop]] = [None]
lock = threading.Lock()
def get_loop() -> asyncio.AbstractEventLoop:
"""Create or return the global DVC event loop."""
if default_loop[0] is None:
with lock:
if default_loop[0] is None:
with _selector_policy():
default_loop[0] = asyncio.new_event_loop()
loop = default_loop[0]
th = threading.Thread(
target=loop.run_forever, # type: ignore[attr-defined]
name="dvcIO",
)
th.daemon = True
th.start()
iothread[0] = th
assert default_loop[0] is not None
return default_loop[0]
class BaseAsyncObject:
def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None):
self._loop: asyncio.AbstractEventLoop = loop or get_loop()
self._pid = os.getpid()
@property
def loop(self):
# AsyncMixin is not fork-safe
assert self._pid == os.getpid()
return self._loop
+4 -10
View File
@@ -149,14 +149,6 @@ class Base:
"""Returns a list of commits in the repo."""
return []
def install(self, use_pre_commit_tool=False):
"""
Adds dvc commands to SCM hooks for the repo.
If use_pre_commit_tool is set and pre-commit is
installed it will be used to install the hooks.
"""
def cleanup_ignores(self):
"""
This method should clean up ignores (eg. entries in .gitignore),
@@ -191,8 +183,10 @@ class Base:
"""
@contextmanager
def track_file_changes(self, config=None):
autostage = get_in(config or {}, ["core", "autostage"])
def track_file_changes(self, config=None, autostage=False):
autostage = get_in(
config or {}, ["core", "autostage"], default=autostage
)
try:
yield
+26 -72
View File
@@ -94,6 +94,12 @@ class Git(Base):
def dir(self):
return first(self.backends.values()).dir
@cached_property
def hooks_dir(self):
from pathlib import Path
return Path(self.root_dir) / self.GIT_DIR / "hooks"
@property
def gitpython(self):
return self.backends["gitpython"]
@@ -195,7 +201,7 @@ class Git(Base):
if not os.path.exists(gitignore):
return
with open(gitignore) as fobj:
with open(gitignore, encoding="utf-8") as fobj:
lines = fobj.readlines()
filtered = list(filter(lambda x: x.strip() != entry.strip(), lines))
@@ -204,69 +210,32 @@ class Git(Base):
os.unlink(gitignore)
return
with open(gitignore, "w") as fobj:
with open(gitignore, "w", encoding="utf-8") as fobj:
fobj.writelines(filtered)
self.track_file(relpath(gitignore))
def _install_hook(self, name):
hook = self._hook_path(name)
with open(hook, "w+") as fobj:
fobj.write(f"#!/bin/sh\nexec dvc git-hook {name} $@\n")
def verify_hook(self, name):
if (self.hooks_dir / name).exists():
raise GitHookAlreadyExistsError(name)
os.chmod(hook, 0o777)
def install_hook(
self, name: str, script: str, interpreter: str = "sh"
) -> None:
import shutil
def _install_merge_driver(self):
self.gitpython.repo.git.config("merge.dvc.name", "DVC merge driver")
self.gitpython.repo.git.config(
"merge.dvc.driver",
(
"dvc git-hook merge-driver "
"--ancestor %O "
"--our %A "
"--their %B "
),
)
self.hooks_dir.mkdir(exist_ok=True)
hook = self.hooks_dir / name
def install(self, use_pre_commit_tool=False):
self._install_merge_driver()
directive = f"#!{shutil.which(interpreter)}"
hook.write_text(f"{directive}\n{script}\n", encoding="utf-8")
hook.chmod(0o777)
if not use_pre_commit_tool:
self._verify_dvc_hooks()
self._install_hook("post-checkout")
self._install_hook("pre-commit")
self._install_hook("pre-push")
return
from dvc.utils.serialize import modify_yaml
config_path = os.path.join(self.root_dir, ".pre-commit-config.yaml")
with modify_yaml(config_path) as config:
entry = {
"repo": "https://github.com/iterative/dvc",
"rev": "master",
"hooks": [
{
"id": "dvc-pre-commit",
"language_version": "python3",
"stages": ["commit"],
},
{
"id": "dvc-pre-push",
"language_version": "python3",
"stages": ["push"],
},
{
"id": "dvc-post-checkout",
"language_version": "python3",
"stages": ["post-checkout"],
"always_run": True,
},
],
}
if entry not in config["repos"]:
config["repos"].append(entry)
def install_merge_driver(
self, name: str, description: str, driver: str
) -> None:
self.gitpython.repo.git.config(f"merge.{name}.name", description)
self.gitpython.repo.git.config(f"merge.{name}.driver", driver)
def cleanup_ignores(self):
for path in self.ignored_paths:
@@ -316,22 +285,6 @@ class Git(Base):
def close(self):
self.backends.close_initialized()
@cached_property
def _hooks_home(self):
return os.path.join(self.root_dir, self.GIT_DIR, "hooks")
def _hook_path(self, name):
return os.path.join(self._hooks_home, name)
def _verify_hook(self, name):
if os.path.exists(self._hook_path(name)):
raise GitHookAlreadyExistsError(name)
def _verify_dvc_hooks(self):
self._verify_hook("post-checkout")
self._verify_hook("pre-commit")
self._verify_hook("pre-push")
@property
def no_commits(self):
return not bool(self.get_ref("HEAD"))
@@ -393,6 +346,7 @@ class Git(Base):
status = partialmethod(_backend_func, "status")
merge = partialmethod(_backend_func, "merge")
validate_git_remote = partialmethod(_backend_func, "validate_git_remote")
check_ref_format = partialmethod(_backend_func, "check_ref_format")
def resolve_rev(self, rev: str) -> str:
from dvc.repo.experiments.utils import exp_refs_by_name
+4 -2
View File
@@ -178,7 +178,7 @@ class BaseGitBackend(ABC):
"""
@abstractmethod
def iter_remote_refs(self, url: str, base: Optional[str] = None):
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
"""Iterate over all refs in the specified remote Git repo.
If base is specified, only refs which begin with base will be yielded.
@@ -197,6 +197,7 @@ class BaseGitBackend(ABC):
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
"""Push refspec to a remote Git repo.
@@ -222,6 +223,7 @@ class BaseGitBackend(ABC):
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
"""Fetch refspecs from a remote Git repo.
@@ -349,5 +351,5 @@ class BaseGitBackend(ABC):
"""
@abstractmethod
def validate_git_remote(self, url: str):
def validate_git_remote(self, url: str, **kwargs):
"""Verify that url is a valid git URL or remote name."""
@@ -23,13 +23,13 @@ from dvc.progress import Tqdm
from dvc.scm.base import GitAuthError, InvalidRemoteSCMRepo, SCMError
from dvc.utils import relpath
from ..objects import GitObject
from .base import BaseGitBackend
from ...objects import GitObject
from ..base import BaseGitBackend
if TYPE_CHECKING:
from dvc.types import StrPath
from ..objects import GitCommit
from ...objects import GitCommit
logger = logging.getLogger(__name__)
@@ -80,6 +80,13 @@ class DulwichObject(GitObject):
class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
"""Dulwich Git backend."""
from dulwich import client
from .asyncssh_vendor import AsyncSSHVendor
# monkeypatch dulwich client's default SSH vendor to use asyncssh
client.get_ssh_vendor = AsyncSSHVendor # type: ignore[assignment]
# Dulwich progress will return messages equivalent to git CLI,
# our pbars should just display the messages as formatted by dulwich
BAR_FMT_NOTOTAL = "{desc}{bar:b}|{postfix[info]} [{elapsed}]"
@@ -354,14 +361,14 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
else:
yield os.fsdecode(key)
def iter_remote_refs(self, url: str, base: Optional[str] = None):
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
from dulwich.client import HTTPUnauthorized, get_transport_and_path
from dulwich.errors import NotGitRepository
from dulwich.porcelain import get_remote_repo
try:
_remote, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
client, path = get_transport_and_path(location, **kwargs)
except Exception as exc:
raise InvalidRemoteSCMRepo(url) from exc
@@ -389,6 +396,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
from dulwich.client import HTTPUnauthorized, get_transport_and_path
from dulwich.errors import NotGitRepository, SendPackError
@@ -402,7 +410,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
try:
_remote, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
client, path = get_transport_and_path(location, **kwargs)
except Exception as exc:
raise SCMError(
f"'{url}' is not a valid Git remote or URL"
@@ -476,6 +484,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
from dulwich.client import get_transport_and_path
from dulwich.objectspec import parse_reftuples
@@ -504,7 +513,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
try:
_remote, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
client, path = get_transport_and_path(location, **kwargs)
except Exception as exc:
raise SCMError(
f"'{url}' is not a valid Git remote or URL"
@@ -659,16 +668,21 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
) -> Optional[str]:
raise NotImplementedError
def validate_git_remote(self, url: str):
def validate_git_remote(self, url: str, **kwargs):
from dulwich.client import LocalGitClient, get_transport_and_path
from dulwich.porcelain import get_remote_repo
try:
_, location = get_remote_repo(self.repo, url)
client, path = get_transport_and_path(location)
client, path = get_transport_and_path(location, **kwargs)
except Exception as exc:
raise InvalidRemoteSCMRepo(url) from exc
if isinstance(client, LocalGitClient) and not os.path.exists(
os.path.join("", path)
):
raise InvalidRemoteSCMRepo(url)
def check_ref_format(self, refname: str):
from dulwich.refs import check_ref_format
return check_ref_format(refname.encode())
@@ -0,0 +1,92 @@
"""asyncssh SSH vendor for Dulwich."""
from typing import List, Optional
from dulwich.client import SSHVendor
from dvc.scm.asyn import BaseAsyncObject, sync_wrapper
class _StderrWrapper:
def __init__(self, stderr):
self.stderr = stderr
async def _readlines(self):
lines = []
while True:
line = await self.stderr.readline()
if not line:
break
lines.append(line)
return lines
readlines = sync_wrapper(_readlines)
class AsyncSSHWrapper(BaseAsyncObject):
def __init__(self, conn, proc, **kwargs):
super().__init__(**kwargs)
self.conn = conn
self.proc = proc
self.stderr = _StderrWrapper(proc.stderr)
def can_read(self) -> bool:
# pylint:disable=protected-access
return self.proc.stdout._session._recv_buf_len > 0
async def _read(self, n: Optional[int] = None) -> bytes:
if self.proc.stdout.at_eof():
return b""
return await self.proc.stdout.read(n=n if n is not None else -1)
read = sync_wrapper(_read)
def write(self, data: bytes):
self.proc.stdin.write(data)
def close(self):
self.conn.close()
class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
def __init__(self, **kwargs):
super().__init__(**kwargs)
async def _run_command(
self,
host: str,
command: List[str],
username: Optional[str] = None,
port: Optional[int] = None,
password: Optional[str] = None,
key_filename: Optional[str] = None,
**kwargs,
):
"""Connect to an SSH server.
Run a command remotely and return a file-like object for interaction
with the remote command.
Args:
host: Host name
command: Command to run (as argv array)
username: Optional ame of user to log in as
port: Optional SSH port to use
password: Optional ssh password for login or private key
key_filename: Optional path to private keyfile
"""
import asyncssh
conn = await asyncssh.connect(
host,
port=port,
username=username,
password=password,
client_keys=[key_filename] if key_filename else [],
known_hosts=None,
encoding=None,
)
proc = await conn.create_process(command, encoding=None)
return AsyncSSHWrapper(conn, proc)
run_command = sync_wrapper(_run_command)
+9 -6
View File
@@ -148,7 +148,8 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
rev: Optional[str] = None,
shallow_branch: Optional[str] = None,
):
import git
from git import Repo
from git.exc import GitCommandError
ld_key = "LD_LIBRARY_PATH"
@@ -170,7 +171,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
url = f"file://{url}"
with TqdmGit(desc="Cloning", unit="obj") as pbar:
clone_from = partial(
git.Repo.clone_from,
Repo.clone_from,
url,
to_path,
env=env, # needed before we can fix it in __init__
@@ -182,7 +183,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
else:
tmp_repo = clone_from(branch=shallow_branch, depth=1)
tmp_repo.close()
except git.exc.GitCommandError as exc: # pylint: disable=no-member
except GitCommandError as exc: # pylint: disable=no-member
raise CloneError(url, to_path) from exc
# NOTE: using our wrapper to make sure that env is fixed in __init__
@@ -191,7 +192,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
if rev:
try:
repo.checkout(rev)
except git.exc.GitCommandError as exc: # pylint: disable=no-member
except GitCommandError as exc: # pylint: disable=no-member
raise RevError(
"failed to access revision '{}' for repo '{}'".format(
rev, url
@@ -438,7 +439,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
for ref in Reference.iter_items(self.repo, common_path=base):
yield ref.path
def iter_remote_refs(self, url: str, base: Optional[str] = None):
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
raise NotImplementedError
def get_refs_containing(self, rev: str, pattern: Optional[str] = None):
@@ -465,6 +466,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
raise NotImplementedError
@@ -474,6 +476,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
raise NotImplementedError
@@ -627,5 +630,5 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
raise SCMError("Merge failed") from exc
return None
def validate_git_remote(self, url: str):
def validate_git_remote(self, url: str, **kwargs):
raise NotImplementedError
+4 -2
View File
@@ -374,6 +374,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
dest: str,
force: bool = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
raise NotImplementedError
@@ -383,6 +384,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
refspecs: Iterable[str],
force: Optional[bool] = False,
on_diverged: Optional[Callable[[str, str], bool]] = None,
**kwargs,
):
raise NotImplementedError
@@ -537,7 +539,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
index.add(entry.path)
index.write()
def iter_remote_refs(self, url: str, base: Optional[str] = None):
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
raise NotImplementedError
def status(
@@ -584,5 +586,5 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
self.repo.index.write()
return None
def validate_git_remote(self, url: str):
def validate_git_remote(self, url: str, **kwargs):
raise NotImplementedError
+1 -1
View File
@@ -273,7 +273,7 @@ class Stage(params.StageParams):
from dvc.output import Output
from dvc.schema import LIVE_PROPS
env[DVCLIVE_PATH] = str(out.path_info)
env[DVCLIVE_PATH] = relpath(out.path_info, self.wdir)
if isinstance(out.live, dict):
config = project(out.live, LIVE_PROPS)

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