Compare commits

...
43 Commits
Author SHA1 Message Date
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
142 changed files with 2385 additions and 2237 deletions
+18 -6
View File
@@ -10,7 +10,9 @@ jobs:
deb:
runs-on: ubuntu-18.04
steps:
- uses: actions/[email protected].4
- uses: actions/[email protected].5
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/[email protected].4
- uses: actions/[email protected].5
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/[email protected].4
- uses: actions/[email protected].5
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/[email protected].4
- uses: actions/[email protected].5
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/[email protected].4
- uses: actions/[email protected].5
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
+9 -5
View File
@@ -20,7 +20,9 @@ jobs:
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}
- uses: actions/[email protected].4
- uses: actions/[email protected].5
with:
fetch-depth: 0
- name: Set up Python 3.8
uses: actions/[email protected]
with:
@@ -28,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
@@ -43,13 +45,15 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-latest, macos-latest]
pyv: ["3.6", "3.7", "3.8", "3.9", "3.10"]
pyv: ["3.7", "3.8", "3.9", "3.10"]
exclude:
# no wheels for pygit2 yet
- os: windows-latest
pyv: "3.10"
steps:
- uses: actions/[email protected].4
- uses: actions/[email protected].5
with:
fetch-depth: 0
- name: Set up Python
uses: actions/[email protected]
with:
@@ -105,7 +109,7 @@ jobs:
run: >-
python -m tests -n=4
--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
+7 -2
View File
@@ -7,6 +7,11 @@ repos:
language_version: python3
repo: https://github.com/ambv/black
rev: 21.9b0
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-toml
- id: check-yaml
- hooks:
- id: codespell
args:
@@ -39,7 +44,7 @@ repos:
- types-tabulate
- types-toml
repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.910'
rev: 'v0.910-1'
- repo: local
hooks:
- id: pylint
@@ -54,7 +59,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
+2 -1
View File
@@ -1,2 +1,3 @@
include fastentrypoints.py
include LICENSE
recursive-include requirements *.txt
recursive-include dvc/repo/plots/templates *.json
+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("")
+23 -3
View File
@@ -382,7 +382,12 @@ def baseline_styler(typ):
def show_experiments(
all_experiments, pager=True, no_timestamp=False, csv=False, **kwargs
all_experiments,
pager=True,
no_timestamp=False,
csv=False,
markdown=False,
**kwargs,
):
from funcy.seqs import flatten as flatten_list
@@ -473,6 +478,7 @@ def show_experiments(
header_styles=styles,
row_styles=row_styles,
csv=csv,
markdown=markdown,
)
@@ -530,6 +536,7 @@ class CmdExperimentsShow(CmdBase):
iso=iso,
pager=not self.args.no_pager,
csv=self.args.csv,
markdown=self.args.markdown,
)
return 0
@@ -579,6 +586,8 @@ class CmdExperimentsDiff(CmdBase):
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
@@ -956,7 +965,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",
)
@@ -986,6 +998,14 @@ def add_parser(subparsers, parent_parser):
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,
@@ -1067,7 +1087,7 @@ def add_parser(subparsers, parent_parser):
"--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",
+90
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,22 @@ class CmdMachineCreate(CmdBase):
return 0
class CmdMachineStatus(CmdBase):
SHOWN_FIELD = ["name", "cloud", "instance_hdd_size", "instance_ip"]
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 +345,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 +383,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",
+2
View File
@@ -75,6 +75,8 @@ class CmdMetricsDiff(CmdMetricsBase):
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
+2
View File
@@ -36,6 +36,8 @@ class CmdParamsDiff(CmdBase):
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
+4 -1
View File
@@ -183,7 +183,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],
+11 -3
View File
@@ -211,8 +211,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 +244,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 +260,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 +272,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):
+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:
+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()
+3 -5
View File
@@ -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)
+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
+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)
)
+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
+1 -1
View File
@@ -393,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"
+5 -7
View File
@@ -6,6 +6,7 @@ import uuid
import psutil
from dvc import __version__
from dvc.exceptions import DvcException, NotDvcRepoError
from dvc.fs import FS_MAP, get_fs_cls, get_fs_config
from dvc.repo import Repo
@@ -13,12 +14,6 @@ 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 +21,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 +91,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 = []
+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
+12 -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,12 @@ 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)
+5 -1
View File
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
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):
@@ -48,3 +48,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."""
+63 -5
View File
@@ -1,9 +1,11 @@
import os
from contextlib import contextmanager
from functools import 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
@@ -12,9 +14,42 @@ if TYPE_CHECKING:
from dvc.types import StrPath
class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
class TerraformBackend(BaseMachineBackend):
def __init__(self, tmp_dir: "StrPath", **kwargs):
super().__init__(tmp_dir)
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(
self, name: Optional[str] = None, **config
@@ -25,7 +60,11 @@ class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
def get_sshfs( # pylint: disable=unused-argument
self, name: Optional[str] = None, **config
) -> Iterator["SSHFileSystem"]:
resource = self._default_resource(name)
from tpi import TerraformProviderIterative
assert name
with self.make_tpi(name) as tpi:
resource = tpi.default_resource(name)
with TerraformProviderIterative.pemfile(resource) as pem:
fs = SSHFileSystem(
host=resource["instance_ip"],
@@ -33,3 +72,22 @@ class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
keyfile=pem,
)
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)
+5 -6
View File
@@ -17,7 +17,6 @@ from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
from dvc.objects.diff import ROOT
from dvc.objects.diff import diff as odiff
from dvc.objects.stage import stage
from dvc.objects.tree import Tree
from dvc.types import Optional
logger = logging.getLogger(__name__)
@@ -131,8 +130,8 @@ def _checkout_file(
):
"""The file is changed we need to checkout a new copy"""
modified = False
cache_info = cache.hash_to_path_info(change.new.obj.hash_info.value)
if change.old.obj:
cache_info = cache.hash_to_path_info(change.new.oid.value)
if change.old.oid:
if relink:
if fs.iscopy(path_info) and _cache_is_copy(cache, path_info):
cache.unprotect(path_info)
@@ -148,7 +147,7 @@ def _checkout_file(
modified = True
if state:
state.save(path_info, fs, change.new.obj.hash_info)
state.save(path_info, fs, change.new.oid)
if progress_callback:
progress_callback(str(path_info))
@@ -166,7 +165,7 @@ def _diff(
):
old = None
try:
_, old = stage(
_, _, old = stage(
cache,
path_info,
fs,
@@ -213,7 +212,7 @@ def _checkout(
if change.new.key != ROOT
else path_info
)
if isinstance(change.new.obj, Tree):
if change.new.oid.isdir:
fs.makedirs(entry_path)
continue
+20 -21
View File
@@ -2,6 +2,8 @@ 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"
@@ -14,22 +16,19 @@ UNCHANGED = "unchanged"
class TreeEntry:
in_cache: bool
key: Tuple[str]
obj: Optional["HashFile"] = field(default=None)
oid: Optional["HashInfo"] = field(default=None)
def __bool__(self):
return bool(self.obj)
return bool(self.oid)
def __eq__(self, other):
if not isinstance(other, TreeEntry):
return False
if self.key != other.key or bool(self.obj) != bool(other.obj):
if self.key != other.key:
return False
if not self.obj:
return False
return self.obj.hash_info == other.obj.hash_info
return self.oid == other.oid
@dataclass
@@ -83,39 +82,39 @@ def diff(
if not obj:
return []
return [ROOT] + (
[key for key, _ in obj] if isinstance(obj, Tree) else []
[key for key, _, _ in obj] if isinstance(obj, Tree) else []
)
old_keys = set(_get_keys(old))
new_keys = set(_get_keys(new))
def _get_obj(obj, key):
def _get_oid(obj, key):
if not obj or key == ROOT:
return obj
return obj.hash_info if obj else None
return obj.get(key)
entry_obj = obj.get(cache, key)
return entry_obj.hash_info if entry_obj else None
def _in_cache(obj, cache):
from . import check
def _in_cache(oid, cache):
from .errors import ObjectFormatError
if not obj:
if not oid:
return False
try:
check(cache, obj)
cache.check(oid)
return True
except (FileNotFoundError, ObjectFormatError):
return False
ret = DiffResult()
for key in old_keys | new_keys:
old_obj = _get_obj(old, key)
new_obj = _get_obj(new, key)
old_oid = _get_oid(old, key)
new_oid = _get_oid(new, key)
change = Change(
old=TreeEntry(_in_cache(old_obj, cache), key, old_obj),
new=TreeEntry(_in_cache(new_obj, cache), key, new_obj),
old=TreeEntry(_in_cache(old_oid, cache), key, old_oid),
new=TreeEntry(_in_cache(new_oid, cache), key, new_oid),
)
if change.typ == ADD:
@@ -126,8 +125,8 @@ def diff(
ret.deleted.append(change)
else:
assert change.typ == UNCHANGED
if not change.new.in_cache and not isinstance(
change.new.obj, Tree
if not change.new.in_cache and not (
change.new.oid and change.new.oid.isdir
):
ret.modified.append(change)
else:
+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
+36 -41
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,7 +39,8 @@ 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):
@@ -54,31 +56,31 @@ def _get_file_hash(path_info, fs, name):
else:
raise NotImplementedError
return HashInfo(name, hash_value, size=info["size"])
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,7 +90,8 @@ def _get_file_obj(
else:
odb.add(path_info, fs, hash_info, move=False)
obj = odb.get(hash_info)
return path_info, obj
return path_info, meta, obj
def _build_objects(
@@ -111,7 +114,7 @@ def _build_objects(
) as pbar:
worker = pbar.wrap_fn(
partial(
_get_file_obj,
_stage_file,
fs=fs,
name=name,
**kwargs,
@@ -130,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)
@@ -143,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)
@@ -155,21 +163,15 @@ 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)
@@ -181,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] = {}
@@ -223,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):
@@ -231,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
@@ -255,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
@@ -273,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
@@ -298,7 +293,7 @@ def stage(
pass
if details["type"] == "directory":
obj = _get_tree_obj(
meta, obj = _stage_tree(
path_info,
fs,
details,
@@ -312,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,
@@ -324,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
+2 -2
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,
+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
+37 -21
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
@@ -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
@@ -550,7 +559,7 @@ class Output:
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 +588,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,
@@ -606,7 +615,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 +623,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,
)
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 +718,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 +812,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,
@@ -832,7 +841,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 +955,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 +990,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 +1009,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 +1022,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 +1056,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,
}
+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 -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:
+1 -1
View File
@@ -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
+11 -9
View File
@@ -163,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
@@ -224,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
@@ -298,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
+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):
+17 -2
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
+1 -1
View File
@@ -490,7 +490,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}"
+1 -1
View File
@@ -64,7 +64,7 @@ class TempDirExecutor(BaseLocalExecutor):
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 cleanup(self):
+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_name == template
or template_name + ".json" == template
):
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"]:
if os.path.exists(name):
return name
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
}
]
}
+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)
+3 -3
View File
@@ -3,7 +3,7 @@ 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
@@ -26,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 = {
+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
+6 -3
View File
@@ -195,7 +195,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,14 +204,14 @@ 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:
with open(hook, "w+", encoding="utf-8") as fobj:
fobj.write(f"#!/bin/sh\nexec dvc git-hook {name} $@\n")
os.chmod(hook, 0o777)
@@ -265,6 +265,9 @@ class Git(Base):
],
}
if "repos" not in config:
config["repos"] = []
if entry not in config["repos"]:
config["repos"].append(entry)
+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
# 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,13 +668,13 @@ 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(
@@ -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)
+4 -2
View File
@@ -438,7 +438,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 +465,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 +475,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 +629,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
+2 -4
View File
@@ -19,13 +19,11 @@ class RunCacheNotFoundError(DvcException):
def _get_cache_hash(cache, key=False):
from dvc.hash_info import HashInfo
from dvc.objects.meta import Meta
if key:
cache["outs"] = [out["path"] for out in cache.get("outs", [])]
return dict_sha256(
cache, exclude=[HashInfo.PARAM_SIZE, HashInfo.PARAM_NFILES]
)
return dict_sha256(cache, exclude=[Meta.PARAM_SIZE, Meta.PARAM_NFILES])
def _can_hash(stage):
+2
View File
@@ -7,6 +7,7 @@ from funcy import cached_property, get_in, lcat, once, project
from dvc import dependency, output
from dvc.hash_info import HashInfo
from dvc.objects.meta import Meta
from dvc.parsing import FOREACH_KWD, JOIN, DataResolver, EntryNotFound
from dvc.parsing.versions import LOCKFILE_VERSION
from dvc.path_info import PathInfo
@@ -69,6 +70,7 @@ class StageLoader(Mapping):
info = info.copy()
info.pop("path", None)
item.isexec = info.pop("isexec", None)
item.meta = Meta.from_dict(info)
item.hash_info = HashInfo.from_dict(info)
@classmethod
+1
View File
@@ -164,6 +164,7 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
ret = [
(item.PARAM_PATH, item.def_path),
*item.hash_info.to_dict().items(),
*item.meta.to_dict().items(),
]
if item.isexec:
+3 -3
View File
@@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Union
from funcy import concat, first, lsplit, rpartial, without
from dvc.exceptions import InvalidArgumentError
from dvc.objects.meta import Meta
from ..hash_info import HashInfo
from .exceptions import (
MissingDataSource,
StageExternalOutputsError,
@@ -197,8 +197,8 @@ def compute_md5(stage):
Output.PARAM_PERSIST,
Output.PARAM_CHECKPOINT,
Output.PARAM_ISEXEC,
HashInfo.PARAM_SIZE,
HashInfo.PARAM_NFILES,
Meta.PARAM_SIZE,
Meta.PARAM_NFILES,
],
)
+7 -5
View File
@@ -38,7 +38,7 @@ class StateNoop(StateBase):
pass
def get(self, path_info, fs): # pylint: disable=unused-argument
return None
return None, None
def save_link(self, path_info, fs):
pass
@@ -103,22 +103,24 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
HashInfo or None: hash for the specified path info or None if it
doesn't exist in the state database.
"""
from .objects.meta import Meta
if not isinstance(fs, LocalFileSystem):
return None
return None, None
try:
mtime, size = get_mtime_and_size(path_info, fs, self.dvcignore)
except FileNotFoundError:
return None
return None, None
inode = get_inode(path_info)
value = self.md5s.get(inode)
if not value or value[0] != mtime or value[1] != str(size):
return None
return None, None
return HashInfo("md5", value[2], size=size)
return Meta(size=size), HashInfo("md5", value[2])
def save_link(self, path_info, fs):
"""Adds the specified path to the list of links created by dvc. This
+2 -2
View File
@@ -60,13 +60,13 @@ class System:
@staticmethod
def _reflink_linux(src, dst):
import fcntl
import fcntl # pylint: disable=import-error
FICLONE = 0x40049409
try:
ret = 255
with open(src) as s, open(dst, "w+") as d:
with open(src, "rb") as s, open(dst, "wb+") as d:
ret = fcntl.ioctl(d.fileno(), FICLONE, s.fileno())
finally:
if ret != 0:
+1 -1
View File
@@ -98,7 +98,7 @@ class Console:
text.overflow = None
return self.write(text, styled=True)
return self.write(json.dumps(data))
return self.write(json.dumps(data, default=default))
def write(
self,
+2 -2
View File
@@ -68,7 +68,7 @@ class Updater:
self.fetch()
return
with open(self.updater_file) as fobj:
with open(self.updater_file, encoding="utf-8") as fobj:
import json
try:
@@ -105,7 +105,7 @@ class Updater:
logger.debug(msg.format(exc))
return
with open(self.updater_file, "w+") as fobj:
with open(self.updater_file, "w+", encoding="utf-8") as fobj:
json.dump(info, fobj)
def _notify(self, latest: str, pkg: Optional[str] = PKG) -> None:
+28 -2
View File
@@ -6,6 +6,16 @@ from typing import Callable, Dict, Iterable, List, TypeVar, Union
from pygtrie import StringTrie as _StringTrie
from dvc.exceptions import DvcException
class NewParamsFound(DvcException):
"""Thrown if new params were found during merge_params"""
def __init__(self, new_params: List, *args):
self.new_params = new_params
super().__init__("New params found during merge", *args)
class PathStringTrie(_StringTrie):
"""Trie based on platform-dependent separator for pathname components."""
@@ -80,11 +90,27 @@ def chunk_dict(d: Dict[_KT, _VT], size: int = 1) -> List[Dict[_KT, _VT]]:
return [{key: d[key] for key in chunk} for chunk in chunks(size, d)]
def merge_params(src: Dict, to_update: Dict) -> Dict:
"""Recursively merges params with benedict's syntax support in-place."""
def merge_params(src: Dict, to_update: Dict, allow_new: bool = True) -> Dict:
"""
Recursively merges params with benedict's syntax support in-place.
Args:
src (dict): source dictionary of parameters
to_update (dict): dictionary of parameters to merge into src
allow_new (bool): if False, raises an error if new keys would be
added to src
"""
from ._benedict import benedict
data = benedict(src)
if not allow_new:
new_params = list(
set(to_update.keys()) - set(data.keypaths(indexes=True))
)
if new_params:
raise NewParamsFound(new_params)
data.merge(to_update, overwrite=True)
return src
+1 -1
View File
@@ -2,4 +2,4 @@ try:
# file is created during dvc build
from .build import PKG # noqa, pylint:disable=unused-import
except ImportError:
PKG = None
PKG = None # type: ignore[assignment]
+1 -3
View File
@@ -47,6 +47,4 @@ class HashedStreamReader(io.IOBase):
@property
def hash_info(self):
return HashInfo(
self.PARAM_CHECKSUM, self.md5.hexdigest(), size=self.total_read
)
return HashInfo(self.PARAM_CHECKSUM, self.md5.hexdigest())
+6 -74
View File
@@ -1,74 +1,6 @@
# Used in setup.py, so don't pull any additional dependencies
#
# Based on:
# - https://github.com/python/mypy/blob/master/mypy/version.py
# - https://github.com/python/mypy/blob/master/mypy/git.py
import os
import subprocess
_BASE_VERSION = "2.8.0"
def _generate_version(base_version):
"""Generate a version with information about the Git repository."""
pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if not _is_git_repo(pkg_dir) or not _have_git():
return base_version
if _is_release(pkg_dir, base_version) and not _is_dirty(pkg_dir):
return base_version
return "{base_version}+{short_sha}{dirty}".format(
base_version=base_version,
short_sha=_git_revision(pkg_dir).decode("utf-8")[0:6],
dirty=".mod" if _is_dirty(pkg_dir) else "",
)
def _is_git_repo(dir_path):
"""Is the given directory version-controlled with Git?"""
return os.path.exists(os.path.join(dir_path, ".git"))
def _have_git():
"""Can we run the git executable?"""
try:
subprocess.check_output(["git", "--help"])
return True
except subprocess.CalledProcessError:
return False
except OSError:
return False
def _is_release(dir_path, base_version):
try:
output = subprocess.check_output(
["git", "describe", "--tags", "--exact-match"],
cwd=dir_path,
stderr=subprocess.STDOUT,
).decode("utf-8")
tag = output.strip()
return tag == base_version
except subprocess.CalledProcessError:
return False
def _git_revision(dir_path):
"""Get SHA of the HEAD of a Git repository."""
return subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=dir_path
).strip()
def _is_dirty(dir_path):
"""Check whether a git repository has uncommitted changes."""
try:
subprocess.check_call(["git", "diff", "--quiet"], cwd=dir_path)
return False
except subprocess.CalledProcessError:
return True
__version__ = _generate_version(_BASE_VERSION)
# pylint: disable=unused-import
try:
from ._dvc_version import version as __version__
from ._dvc_version import version_tuple # noqa: F401
except ImportError:
__version__ = "UNKNOWN"
-121
View File
@@ -1,121 +0,0 @@
# noqa: D300,D400
# Copyright (c) 2016, Aaron Christianson
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
Monkey patch setuptools to write faster console_scripts with this format:
import sys
from mymodule import entry_function
sys.exit(entry_function())
This is better.
(c) 2016, Aaron Christianson
http://github.com/ninjaaron/fast-entry_points
"""
import re
from setuptools.command import easy_install
TEMPLATE = r"""
# -*- coding: utf-8 -*-
# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}'
__requires__ = '{3}'
import re
import sys
from {0} import {1}
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
sys.exit({2}())
""".lstrip()
@classmethod
def get_args(cls, dist, header=None): # noqa: D205,D400
"""
Yield write_script() argument tuples for a distribution's
console_scripts and gui_scripts entry points.
"""
if header is None:
# pylint: disable=E1101
header = cls.get_header()
spec = str(dist.as_requirement())
for type_ in "console", "gui":
group = type_ + "_scripts"
for name, ep in dist.get_entry_map(group).items():
# ensure_safe_name
if re.search(r"[\\/]", name):
raise ValueError("Path separators not allowed in script names")
script_text = TEMPLATE.format(
ep.module_name,
ep.attrs[0],
".".join(ep.attrs),
spec,
group,
name,
)
# pylint: disable=E1101,protected-access
args = cls._get_script_args(type_, name, header, script_text)
yield from args
# pylint: disable=E1101
easy_install.ScriptWriter.get_args = get_args
def main():
import os
import shutil
import sys
dests = sys.argv[1:] or ["."]
filename = re.sub(r"\.pyc$", ".py", __file__)
for dst in dests:
shutil.copy(filename, dst)
manifest_path = os.path.join(dst, "MANIFEST.in")
setup_path = os.path.join(dst, "setup.py")
# Insert the include statement to MANIFEST.in if not present
with open(manifest_path, "a+") as manifest:
manifest.seek(0)
manifest_content = manifest.read()
if "include fastentrypoints.py" not in manifest_content:
manifest.write(
("\n" if manifest_content else "")
+ "include fastentrypoints.py"
)
# Insert the import statement to setup.py if not present
with open(setup_path, "a+") as setup:
setup.seek(0)
setup_content = setup.read()
if "import fastentrypoints" not in setup_content:
setup.seek(0)
setup.truncate()
setup.write("import fastentrypoints\n" + setup_content)
+62
View File
@@ -1,3 +1,10 @@
[build-system]
requires = ["setuptools>=48", "wheel", "setuptools_scm[toml]>=6.3.1"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "dvc/_dvc_version.py"
[tool.black]
line-length = 79
include = '\.pyi?$'
@@ -15,3 +22,58 @@ exclude = '''
| dist
)/
'''
[tool.isort]
profile = "black"
known_first_party = ["dvc", "tests"]
line_length = 79
[tool.pytest.ini_options]
log_level = "debug"
addopts = "-ra"
markers = [
"needs_internet: Might need network access for the tests",
]
[tool.mypy]
# Error output
show_column_numbers = true
show_error_codes = true
show_error_context = true
show_traceback = true
pretty = true
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
ignore_missing_imports = true
check_untyped_defs = false
# Warnings
warn_no_return = true
warn_redundant_casts = true
warn_unreachable = true
files = ["dvc"]
[tool.pylint.master]
extension-pkg-whitelist = ["pygit2"]
init-hook = "import sys; sys.path.append(str('tests'))"
load-plugins = ["pylint_pytest", "pylint_plugin_disable"]
[tool.pylint.message_control]
disable = [
"format", "refactoring", "spelling", "design", "no-self-use",
"invalid-name", "misplaced-comparison-constant", "duplicate-code", "fixme",
"unused-wildcard-import", "cyclic-import", "wrong-import-order",
"wrong-import-position", "ungrouped-imports", "multiple-imports",
"logging-format-interpolation", "logging-fstring-interpolation",
"missing-function-docstring", "missing-module-docstring",
"missing-class-docstring", "raise-missing-from", "import-outside-toplevel",
]
enable = ["c-extension-no-member", "no-else-return"]
[tool.pylint.typecheck]
generated-members = ["pytest.lazy_fixture", "logging.TRACE", "logger.trace", "sys.getwindowsversion", "argparse.Namespace"]
ignored-classes = ["Dvcfile"]
ignored-modules = ["azure"]
signature-mutators = ["funcy.decorators.decorator"]
[tool.pylint.variables]
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
ignored-argument-names = "_.*|^ignored_|^unused_|args|kwargs"
+2 -2
View File
@@ -10,5 +10,5 @@ fi
echo 'PKG = "pip"' >dvc/utils/build.py
python setup.py sdist
python setup.py bdist_wheel
python -m pip install -U build setuptools>=58.2
python -m build --sdist --wheel --outdir dist/
+2 -1
View File
@@ -9,5 +9,6 @@ fi
rm -rf dist/
pip install twine
python setup.py sdist bdist_wheel
python -m pip install -U build setuptools>=58.2
python -m build --sdist --wheel --outdir dist/
twine upload dist/*
+26 -57
View File
@@ -2,7 +2,7 @@
description = Git for data scientists - manage your code and data together
name = dvc
long_description = file: README.rst
long_description_content_type = text/rst
long_description_content_type = text/x-rst
license = Apache License 2.0
license_file = LICENSE
url = http://dvc.org
@@ -19,14 +19,16 @@ keywords = data-science, data-version-control, machine-learning, git
classifiers =
Development Status :: 4 - Beta
Programming Language :: Python :: 3
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
[options]
python_requires = >=3.6
setup_requires =
setuptools>=48
setuptools_scm[toml]>=6.3.1
python_requires = >=3.7
zip_safe = False
packages = find:
include_package_data = True
@@ -37,9 +39,7 @@ install_requires =
configobj>=5.0.6
gitpython>3
dulwich>=0.20.23
pygit2>=1.5.0,<1.7; python_version < '3.7'
pygit2>=1.5.0; python_version >= '3.7'
setuptools>=34.0.0
pygit2>=1.5.0
nanotime>=0.5.2
pyasn1>=0.4.1
voluptuous>=0.11.7
@@ -51,19 +51,17 @@ install_requires =
ruamel.yaml>=0.17.11
toml>=0.10.1
funcy>=1.14
pathspec>=0.6.0,<0.9.0
pathspec>=0.9.0,<0.10.0
shortuuid>=0.5.0
tqdm>=4.45.0,<5
packaging>=19.0
zc.lockfile>=1.2.1
flufl.lock>=3.2,<4
win-unicode-console>=0.5; sys_platform == 'win32'
flufl.lock>=5
networkx>=2.5
psutil>=5.8.0
pydot>=1.2.4
dataclasses>=0.7; python_version < '3.7'
contextvars>=2.1; python_version < '3.7'
importlib-metadata>=1.4; python_version < '3.8'
importlib-resources>=5.2.2; python_version < '3.9'
flatten_dict>=0.4.1,<1
tabulate>=0.8.7
pygtrie>=2.3.2
@@ -74,7 +72,7 @@ install_requires =
python-benedict>=0.24.2
pyparsing==2.4.7
typing_extensions>=3.7.4
fsspec[http]>=2021.10.0
fsspec[http]>=2021.10.1
aiohttp-retry>=2.4.5
diskcache>=5.2.1
@@ -101,19 +99,19 @@ azure =
azure-identity>=1.4.0
knack
gdrive = pydrive2[fsspec]>=1.9.4
gs = gcsfs==2021.10.0
gs = gcsfs==2021.10.1
hdfs =
fsspec[arrow]; python_version < '3.10'
oss = ossfs==2021.8.0
s3 =
s3fs==2021.10.0
s3fs==2021.10.1
aiobotocore[boto3]>1.0.1
ssh = sshfs[bcrypt]>=2021.8.1
ssh_gssapi = sshfs[gssapi]>=2021.8.1
webdav = webdav4>=0.9.1
webdav = webdav4>=0.9.3
# not to break `dvc[webhdfs]`
webhdfs =
terraform = tpi[ssh]>=0.0.0
terraform = tpi[ssh]>=2.1.0
tests =
%(terraform)s
wheel==0.37.0
@@ -129,14 +127,13 @@ tests =
rangehttpserver==1.2.0
mock-ssh-server==0.9.1
wget==3.2
filelock==3.3.0
filelock==3.3.1
wsgidav==3.1.1
crc32c==2.2.post0
xmltodict==0.12.0
google-compute-engine==2.8.13
google-cloud-storage==1.42.3
dvclive==0.4.0
pywin32>=225; sys_platform == 'win32' and python_version < '3.10'
dvclive==0.4.1
hdfs==2.6.0
# required by collective.checkdocs
Pygments==2.10.0
@@ -144,7 +141,7 @@ tests =
pydocstyle==6.1.1
jaraco.windows==5.7.0
# pylint requirements
pylint==2.9.6
pylint==2.11.1
# we use this to suppress pytest-related false positives in our tests.
pylint-pytest==1.0.3
# we use this to suppress some messages in tests, eg: foo/bar naming,
@@ -152,10 +149,11 @@ tests =
pylint-plugin-utils==0.6
# type-checking
mypy==0.910
types-requests==2.25.9
types-paramiko==2.7.1
types-tabulate==0.8.2
types-toml==0.10.0
types-requests==2.25.11
types-tabulate==0.8.3
types-toml==0.10.1
# optional dependencies
pywin32>=225; sys_platform == 'win32'
[options.packages.find]
exclude =
@@ -173,37 +171,8 @@ ignore=
W503, # Line break occurred before a binary operator
P1, # unindexed parameters in the str.format, see:
# https://pypi.org/project/flake8-string-format/
max_line_length=79
max_line_length = 79
max-complexity = 15
select=B,C,E,F,W,T4,B902,T,P
show_source=true
count=true
[isort]
profile=black
known_first_party=dvc,tests
line_length=79
[tool:pytest]
log_level = debug
addopts = -ra
markers =
needs_internet: Might need network access for the tests
[mypy]
# Error output
show_column_numbers = True
show_error_codes = True
show_error_context = True
show_traceback = True
pretty = True
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
ignore_missing_imports = True
check_untyped_defs = False
# Warnings
warn_no_return=True
warn_redundant_casts=True
warn_unreachable = True
files = dvc
select = B,C,E,F,W,T4,B902,T,P
show_source = true
count = true
+1 -64
View File
@@ -1,66 +1,3 @@
import importlib.util
import os
from pathlib import Path
from textwrap import dedent
from setuptools import setup
from setuptools.command.build_py import build_py as _build_py
# Prevents pkg_resources import in entry point script,
# see https://github.com/ninjaaron/fast-entry_points.
# This saves about 200 ms on startup time for non-wheel installs.
try:
import fastentrypoints # noqa: F401, pylint: disable=unused-import
except ImportError:
pass # not able to import when installing through pre-commit
# Read package meta-data from version.py
# see https://packaging.python.org/guides/single-sourcing-package-version/
try:
pkg_dir = os.path.dirname(os.path.abspath(__file__))
version_path = os.path.join(pkg_dir, "dvc", "version.py")
spec = importlib.util.spec_from_file_location("dvc.version", version_path)
dvc_version = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dvc_version)
version = dvc_version.__version__ # noqa: F821
except Exception as exc: # pylint: disable=broad-except
# Dependabot seem to stop working when we don't handle this except block.
# Most likely, it's because of the restrictions in execution of the module.
# We workaround it, and print the error message if it also happens on other
# installations (though this message may likely be suppressed by build
# tools, eg: pip only shows this message on `--verbose` mode).
import sys
print("Could not load version info: ", exc, file=sys.stderr)
version = "UNKNOWN"
# To achieve consistency between the build version and the one provided
# by your package during runtime, you need to **pin** the build version.
#
# This custom class will replace the version.py module with a **static**
# `__version__` that your package can read at runtime, assuring consistency.
#
# References:
# - https://docs.python.org/3.7/distutils/extending.html
# - https://github.com/python/mypy
class build_py(_build_py):
def pin_version(self):
ver_file = Path(self.build_lib) / "dvc" / "version.py"
self.mkpath(os.fspath(ver_file.parent))
ver_file.write_text(
dedent(
f"""\
# AUTOGENERATED at build time by setup.py
__version__ = "{version}"
"""
)
)
def run(self):
self.execute(self.pin_version, ())
_build_py.run(self)
setup(version=version, cmdclass={"build_py": build_py})
setup()
+7 -4
View File
@@ -5,9 +5,12 @@ import sys
if os.name == "nt":
import subprocess
import win32file # pylint: disable=import-error
win32file._setmaxstdio(4096)
try:
import win32file # pylint: disable=import-error
except ImportError:
pass
else:
win32file._setmaxstdio(4096)
# Workaround for two bugs:
#
@@ -34,7 +37,7 @@ if os.name == "nt":
subprocess._cleanup = noop
subprocess._active = None
else:
import resource
import resource # pylint: disable=import-error
resource.setrlimit(resource.RLIMIT_NOFILE, (4096, 4096))
+11 -2
View File
@@ -161,10 +161,19 @@ def pytest_configure(config):
@pytest.fixture()
def custom_template(tmp_dir, dvc):
import shutil
try:
import importlib_resources
except ImportError:
import importlib.resources as importlib_resources
content = (
importlib_resources.files("dvc.repo.plots")
/ "templates"
/ "simple.json"
).read_text()
template = tmp_dir / "custom_template.json"
shutil.copy(tmp_dir / ".dvc" / "plots" / "default.json", template)
template.write_text(content)
return template
+1 -1
View File
@@ -247,7 +247,7 @@ class TmpDir(pathlib.Path):
path.name: path.read_text(*args, **kwargs)
for path in self.iterdir()
}
return super().read_text(*args, **kwargs)
return super().read_text(*args, encoding="utf-8", **kwargs)
def hash_to_path_info(self, hash_):
return self / hash_[0:2] / hash_[2:]
+11
View File
@@ -37,3 +37,14 @@ services:
- 2222
volumes:
- ./remotes/user.key.pub:/tmp/key
git-server:
image: ghcr.io/linuxserver/openssh-server
environment:
- USER_NAME=user
- PUBLIC_KEY_FILE=/tmp/key
ports:
- 2222
volumes:
- ./remotes/user.key.pub:/tmp/key
- ./remotes/git-init:/config/custom-cont-init.d
+24 -5
View File
@@ -7,6 +7,7 @@ import pytest
from funcy import first
from dvc.dvcfile import PIPELINE_FILE
from dvc.exceptions import DvcException
from dvc.repo.experiments.utils import exp_refs_by_rev
from dvc.utils.serialize import PythonFileCorruptedError
from tests.func.test_repro_multistage import COPY_SCRIPT
@@ -118,10 +119,6 @@ def test_failed_exp(tmp_dir, scm, dvc, exp_stage, mocker, caplog):
["foo[1]=[baz, goo]"],
"{foo: [bar: 1, [baz, goo]], goo: {bag: 3}, lorem: false}",
],
[
["lorem.ipsum=3"],
"{foo: [bar: 1, baz: 2], goo: {bag: 3}, lorem: {ipsum: 3}}",
],
],
)
def test_modify_params(tmp_dir, scm, dvc, mocker, changes, expected):
@@ -148,6 +145,28 @@ def test_modify_params(tmp_dir, scm, dvc, mocker, changes, expected):
assert fobj.read().strip() == expected
@pytest.mark.parametrize(
"changes",
[["lorem.ipsum=3"], ["foo[0].bazar=3"]],
)
def test_add_params(tmp_dir, scm, dvc, changes):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen(
"params.yaml", "{foo: [bar: 1, baz: 2], goo: {bag: 3}, lorem: false}"
)
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
metrics_no_cache=["metrics.yaml"],
params=["foo", "goo", "lorem"],
name="copy-file",
)
scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
scm.commit("init")
with pytest.raises(DvcException):
dvc.experiments.run(stage.addressing, params=changes)
@pytest.mark.parametrize("queue", [True, False])
def test_apply(tmp_dir, scm, dvc, exp_stage, queue):
from dvc.exceptions import InvalidArgumentError
@@ -387,7 +406,7 @@ def test_no_scm(tmp_dir):
@pytest.mark.parametrize("workspace", [True, False])
def test_untracked(tmp_dir, scm, dvc, caplog, workspace):
tmp_dir.gen("copy.py", COPY_SCRIPT)
tmp_dir.gen("params.yaml", "foo: 1")
tmp_dir.scm_gen("params.yaml", "foo: 1", commit="track params")
stage = dvc.run(
cmd="python copy.py params.yaml metrics.yaml",
metrics_no_cache=["metrics.yaml"],
+2 -2
View File
@@ -242,7 +242,7 @@ def test_push_pull_cache(
hash_ = digest(str(x))
path = os.path.join(local_remote.url, hash_[:2], hash_[2:])
assert os.path.exists(path)
assert open(path).read() == str(x)
assert open(path, encoding="utf-8").read() == str(x)
remove(dvc.odb.local.cache_dir)
@@ -251,7 +251,7 @@ def test_push_pull_cache(
hash_ = digest(str(x))
path = os.path.join(dvc.odb.local.cache_dir, hash_[:2], hash_[2:])
assert os.path.exists(path)
assert open(path).read() == str(x)
assert open(path, encoding="utf-8").read() == str(x)
def test_auth_error_list(tmp_dir, scm, dvc, http_auth_patch):
+1 -1
View File
@@ -477,7 +477,7 @@ def test_show_with_broken_repo(tmp_dir, scm, dvc, exp_stage, caplog):
exp1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
exp2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
with open("dvc.yaml", "a") as fd:
with open("dvc.yaml", "a", encoding="utf-8") as fd:
fd.write("breaking the yaml!")
result = dvc.experiments.show()
+10
View File
@@ -0,0 +1,10 @@
import textwrap
CONFIG_TEXT = textwrap.dedent(
"""\
[feature]
machine = true
['machine "foo"']
cloud = aws
"""
)
+61 -8
View File
@@ -2,17 +2,11 @@ import os
import textwrap
import pytest
import tpi
from dvc.main import main
CONFIG_TEXT = textwrap.dedent(
"""\
[feature]
machine = true
['machine \"foo\"']
cloud = aws
"""
)
from . import CONFIG_TEXT
@pytest.mark.parametrize(
@@ -121,3 +115,62 @@ def test_machine_list(tmp_dir, dvc, capsys):
)
in cap.out
)
def test_machine_rename_success(tmp_dir, scm, dvc, capsys, mocker):
config_file = tmp_dir / ".dvc" / "config"
config_file.write_text(CONFIG_TEXT)
mocker.patch.object(
tpi.terraform.TerraformBackend,
"state_mv",
autospec=True,
return_value=True,
)
os.makedirs((tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "foo"))
assert main(["machine", "rename", "foo", "bar"]) == 0
cap = capsys.readouterr()
assert "Rename machine 'foo' to 'bar'." in cap.out
assert config_file.read_text() == CONFIG_TEXT.replace("foo", "bar")
assert not (
tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "foo"
).exists()
assert (
tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "bar"
).exists()
def test_machine_rename_none_exist(tmp_dir, scm, dvc, caplog):
config_alice = CONFIG_TEXT.replace("foo", "alice")
config_file = tmp_dir / ".dvc" / "config"
config_file.write_text(config_alice)
assert main(["machine", "rename", "foo", "bar"]) == 251
assert config_file.read_text() == config_alice
assert "machine 'foo' doesn't exist." in caplog.text
def test_machine_rename_exist(tmp_dir, scm, dvc, caplog):
config_bar = CONFIG_TEXT + "['machine \"bar\"']\n cloud = aws"
config_file = tmp_dir / ".dvc" / "config"
config_file.write_text(config_bar)
assert main(["machine", "rename", "foo", "bar"]) == 251
assert config_file.read_text() == config_bar
assert "Machine 'bar' already exists." in caplog.text
def test_machine_rename_error(tmp_dir, scm, dvc, caplog, mocker):
config_file = tmp_dir / ".dvc" / "config"
config_file.write_text(CONFIG_TEXT)
os.makedirs((tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "foo"))
def cmd_error(self, source, destination, **kwargs):
raise tpi.TPIError("test error")
mocker.patch.object(tpi.terraform.TerraformBackend, "state_mv", cmd_error)
assert main(["machine", "rename", "foo", "bar"]) == 251
assert config_file.read_text() == CONFIG_TEXT
assert "rename failed" in caplog.text
+43
View File
@@ -0,0 +1,43 @@
import json
from tpi.terraform import TerraformBackend
from dvc.command.machine import CmdMachineStatus
from dvc.main import main
from . import CONFIG_TEXT
STATUS = """{
"aws_security_group": null,
"cloud": "aws",
"id": "iterative-2jyhw8j9ieov6",
"image": "ubuntu-bionic-18.04-amd64-server-20210818",
"instance_gpu": null,
"instance_hdd_size": 35,
"instance_ip": "123.123.123.123",
"instance_launch_time": "2021-08-25T07:13:03Z",
"instance_type": "m",
"name": "test-resource",
"region": "us-west",
"spot": false,
"spot_price": -1,
"ssh_name": null,
"ssh_private": "-----BEGIN RSA PRIVATE KEY-----\\n",
"startup_script": "IyEvYmluL2Jhc2g=",
"timeouts": null
}"""
def test_status(tmp_dir, scm, dvc, mocker, capsys):
(tmp_dir / ".dvc" / "config").write_text(CONFIG_TEXT)
status = json.loads(STATUS)
mocker.patch.object(
TerraformBackend, "instances", autospec=True, return_value=[status]
)
assert main(["machine", "status", "foo"]) == 0
cap = capsys.readouterr()
assert "instance_num_1:" in cap.out
for key in CmdMachineStatus.SHOWN_FIELD:
assert f"\t{key:20}: {status[key]}" in cap.out
+3 -2
View File
@@ -187,9 +187,10 @@ def test_metrics_diff_cli(tmp_dir, scm, dvc, run_copy_metrics, caplog, capsys):
assert main(["metrics", "diff", "HEAD~2"]) == 0
captured = capsys.readouterr()
assert captured.out == (
"Path Metric Old New Change\n"
"m.yaml foo 1.23457 3.45679 2.22222\n"
"Path Metric HEAD~2 workspace Change\n"
"m.yaml foo 1.23457 3.45679 2.22222\n"
)
+1 -1
View File
@@ -277,7 +277,7 @@ def test_log_errors(
)
scm.tag("v1")
with open(file, "a") as fd:
with open(file, "a", encoding="utf-8") as fd:
fd.write("\nMALFORMED!")
result = dvc.metrics.show(revs=["v1"])
+4 -4
View File
@@ -16,7 +16,7 @@ def index(dvc, local_remote, mocker):
def test_indexed_on_status(tmp_dir, dvc, index):
foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0]
bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0]
baz_hash = bar.obj.trie.get(("baz",)).hash_info
baz_hash = bar.obj.trie.get(("baz",))[1]
clean_staging()
dvc.push()
index.clear()
@@ -30,7 +30,7 @@ def test_indexed_on_status(tmp_dir, dvc, index):
def test_indexed_on_push(tmp_dir, dvc, index):
foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0]
bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0]
baz_hash = bar.obj.trie.get(("baz",)).hash_info
baz_hash = bar.obj.trie.get(("baz",))[1]
clean_staging()
dvc.push()
@@ -60,8 +60,8 @@ def test_clear_on_download_err(tmp_dir, dvc, index, mocker):
out = tmp_dir.dvc_gen({"dir": {"foo": "foo content"}})[0].outs[0]
dvc.push()
for _, entry in out.obj:
remove(dvc.odb.local.get(entry.hash_info).path_info)
for _, _, oid in out.obj:
remove(dvc.odb.local.get(oid).path_info)
remove(out.path_info)
assert list(index.hashes())
+1 -1
View File
@@ -158,7 +158,7 @@ def test_log_errors(tmp_dir, scm, dvc, capsys, file, error_path):
)
rename = (tmp_dir / file).read_text()
with open(tmp_dir / file, "a") as fd:
with open(tmp_dir / file, "a", encoding="utf-8") as fd:
fd.write("\nmalformed!")
scm.add([PIPELINE_FILE, "params_other.yaml"])
+7 -7
View File
@@ -54,7 +54,7 @@ def test_plot_wrong_metric_type(tmp_dir, scm, dvc, run_copy_metrics):
)
assert isinstance(
dvc.plots.collect(targets=["metric.txt"], onerror=onerror_collect)[
dvc.plots.show(targets=["metric.txt"], onerror=onerror_collect)[
"workspace"
]["data"]["metric.txt"]["error"],
PlotMetricTypeError,
@@ -148,7 +148,7 @@ def test_plots_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
dvc._reset()
assert isinstance(
dvc.plots.collect(onerror=onerror_collect)["workspace"]["error"],
dvc.plots.show(onerror=onerror_collect)["workspace"]["error"],
OverlappingOutputPathsError,
)
@@ -183,12 +183,12 @@ def test_dir_plots(tmp_dir, dvc, run_copy_metrics):
assert result["workspace"]["data"][p2]["props"] == props
def test_ignore_binary_file(tmp_dir, dvc, run_copy_metrics):
with open("file", "wb") as fobj:
def test_ignore_parsing_error(tmp_dir, dvc, run_copy_metrics):
with open("file", "wb", encoding=None) as fobj:
fobj.write(b"\xc1")
run_copy_metrics("file", "plot_file.json", plots=["plot_file.json"])
result = dvc.plots.collect(onerror=onerror_collect)
result = dvc.plots.show(onerror=onerror_collect)
assert isinstance(
result["workspace"]["data"]["plot_file.json"]["error"], EncodingError
@@ -216,10 +216,10 @@ def test_log_errors(
)
scm.tag("v1")
with open(file, "a") as fd:
with open(file, "a", encoding="utf-8") as fd:
fd.write("\nMALFORMED!")
result = dvc.plots.collect(onerror=onerror_collect)
result = dvc.plots.show(onerror=onerror_collect)
_, error = capsys.readouterr()
assert isinstance(get_in(result, error_path), YAMLFileCorruptedError)
+16 -11
View File
@@ -87,7 +87,7 @@ def test_add_executable(tmp_dir, dvc):
def test_add_unicode(tmp_dir, dvc):
with open("\xe1", "wb") as fd:
with open("\xe1", "wb", encoding=None) as fd:
fd.write(b"something")
(stage,) = dvc.add("\xe1")
@@ -112,7 +112,7 @@ def test_add_directory(tmp_dir, dvc):
hash_info = stage.outs[0].hash_info
obj = load(dvc.odb.local, hash_info)
for key, _ in obj:
for key, _, _ in obj:
for part in key:
assert "\\" not in part
@@ -143,7 +143,7 @@ class TestAddCmdDirectoryRecursive(TestDvc):
# Create a lot of files
for iteration in range(LARGE_DIR_SIZE + 1):
path = os.path.join("large-dir", str(iteration))
with open(path, "w") as fobj:
with open(path, "w", encoding="utf-8") as fobj:
fobj.write(path)
assert main(["add", "--recursive", "large-dir"]) == 0
@@ -534,7 +534,7 @@ class TestAddCommit(TestDvc):
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
tmp_dir.gen({"data/data": "foo"})
counter = mocker.spy(dvc_module.objects.stage, "_get_tree_obj")
counter = mocker.spy(dvc_module.objects.stage, "_stage_tree")
ret = main(["add", "data"])
assert ret == 0
assert counter.mock.call_count == 2
@@ -581,7 +581,7 @@ class TestShouldThrowProperExceptionOnCorruptedStageFile(TestDvc):
foo_stage = relpath(self.FOO + DVC_FILE_SUFFIX)
# corrupt stage file
with open(foo_stage, "a+") as file:
with open(foo_stage, "a+", encoding="utf-8") as file:
file.write("this will break yaml file structure")
self._caplog.clear()
@@ -677,8 +677,12 @@ def temporary_windows_drive(tmp_path_factory):
import string
from ctypes import windll
import win32api # pylint: disable=import-error
from win32con import DDD_REMOVE_DEFINITION # pylint: disable=import-error
try:
# pylint: disable=import-error
import win32api
from win32con import DDD_REMOVE_DEFINITION
except ImportError:
pytest.skip("pywin32 not installed")
drives = [
s[0].upper()
@@ -999,7 +1003,7 @@ def test_add_preserve_meta(tmp_dir, dvc):
# are the same 260 chars, which makes the test unnecessarily complex
@pytest.mark.skipif(os.name == "nt", reason="unsupported on Windows")
def test_add_long_fname(tmp_dir, dvc):
name_max = os.pathconf(tmp_dir, "PC_NAME_MAX")
name_max = os.pathconf(tmp_dir, "PC_NAME_MAX") # pylint: disable=no-member
name = "a" * name_max
tmp_dir.gen({"data": {name: "foo"}})
@@ -1026,8 +1030,9 @@ def test_add_to_remote(tmp_dir, dvc, local_cloud, local_remote):
assert len(stage.outs) == 1
hash_info = stage.outs[0].hash_info
meta = stage.outs[0].meta
assert local_remote.hash_to_path_info(hash_info.value).read_text() == "foo"
assert hash_info.size == len("foo")
assert meta.size == len("foo")
def test_add_to_remote_absolute(tmp_dir, make_tmp_dir, dvc, local_remote):
@@ -1071,8 +1076,8 @@ def test_add_to_cache_dir(tmp_dir, dvc, local_cloud):
(stage,) = dvc.add(str(local_cloud / "data"), out="data")
assert len(stage.deps) == 0
assert len(stage.outs) == 1
assert stage.outs[0].hash_info.size == len("foo") + len("bar")
assert stage.outs[0].hash_info.nfiles == 2
assert stage.outs[0].meta.size == len("foo") + len("bar")
assert stage.outs[0].meta.nfiles == 2
data = tmp_dir / "data"
assert data.read_text() == {"foo": "foo", "bar": "bar"}
+9 -9
View File
@@ -73,7 +73,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
cache = self.foo_stage.outs[0].cache_path
os.chmod(cache, 0o644)
with open(cache, "a") as fd:
with open(cache, "a", encoding="utf-8") as fd:
fd.write("1")
with pytest.raises(CheckoutError):
@@ -101,13 +101,13 @@ class TestCheckoutCorruptedCacheDir(TestDvc):
# NOTE: modifying cache file for one of the files inside the directory
# to check if dvc will detect that the cache is corrupted.
obj = load(self.dvc.odb.local, out.hash_info)
_, entry_obj = list(obj)[0]
_, _, entry_oid = list(obj)[0]
cache = os.fspath(
self.dvc.odb.local.hash_to_path_info(entry_obj.hash_info.value)
self.dvc.odb.local.hash_to_path_info(entry_oid.value)
)
os.chmod(cache, 0o644)
with open(cache, "w+") as fobj:
with open(cache, "w+", encoding="utf-8") as fobj:
fobj.write("1")
with pytest.raises(CheckoutError):
@@ -127,7 +127,7 @@ class CheckoutBase(TestDvcGit):
GIT_IGNORE = ".gitignore"
def commit_data_file(self, fname, content="random text"):
with open(fname, "w") as fd:
with open(fname, "w", encoding="utf-8") as fd:
fd.write(content)
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
@@ -136,7 +136,7 @@ class CheckoutBase(TestDvcGit):
self.dvc.scm.commit("adding " + fname)
def read_ignored(self):
with open(self.GIT_IGNORE) as f:
with open(self.GIT_IGNORE, encoding="utf-8") as f:
return [s.strip("\n") for s in f.readlines()]
def outs_info(self, stage):
@@ -188,7 +188,7 @@ class TestCheckoutCleanWorkingDir(CheckoutBase):
stage = stages[0]
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
with open(working_dir_change, "w") as f:
with open(working_dir_change, "w", encoding="utf-8") as f:
f.write("not_cached")
ret = main(["checkout", stage.relpath])
@@ -204,7 +204,7 @@ class TestCheckoutCleanWorkingDir(CheckoutBase):
stage = stages[0]
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
with open(working_dir_change, "w") as f:
with open(working_dir_change, "w", encoding="utf-8") as f:
f.write("not_cached")
ret = main(["checkout", stage.relpath])
@@ -610,7 +610,7 @@ def test_checkout_stats_on_failure(tmp_dir, dvc, scm):
# corrupt cache
cache = stage.outs[0].cache_path
os.chmod(cache, 0o644)
with open(cache, "a") as fd:
with open(cache, "a", encoding="utf-8") as fd:
fd.write("destroy cache")
scm.checkout("HEAD~")
+3 -5
View File
@@ -57,9 +57,7 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
out_dir = stage_dir.outs[0]
cache_dir = out_dir.cache_path
dir_hash = out_dir.hash_info
dir_hashes = {dir_hash} | {
entry_obj.hash_info for _, entry_obj in out_dir.obj
}
dir_hashes = {dir_hash} | {oid for _, _, oid in out_dir.obj}
def _check_status(status, **kwargs):
for key in ("ok", "missing", "new", "deleted"):
@@ -114,7 +112,7 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
dvc.cloud.pull(foo_hashes)
assert os.path.exists(cache)
assert os.path.isfile(cache)
with open(cache) as fd:
with open(cache, encoding="utf-8") as fd:
assert fd.read() == "foo"
dvc.cloud.pull(dir_hashes)
@@ -168,7 +166,7 @@ def test_cloud_cli(tmp_dir, dvc, remote):
assert os.path.isfile("foo")
assert os.path.isdir("data_dir")
with open(cache) as fd:
with open(cache, encoding="utf-8") as fd:
assert fd.read() == "foo"
assert os.path.isfile(cache_dir)
+1 -1
View File
@@ -203,7 +203,7 @@ def test_subrepos_are_ignored(tmp_dir, erepo_dir):
clean_staging()
makedirs(cache_dir)
staging, obj = stage(
staging, _, obj = stage(
repo.odb.local,
PathInfo(repo.root_dir) / "dir",
repo.repo_fs,
+8 -8
View File
@@ -60,7 +60,7 @@ class TestGCBranchesTags(TestDvcGit):
def test(self):
fname = "file"
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("v1.0")
stages = self.dvc.add(fname)
@@ -71,7 +71,7 @@ class TestGCBranchesTags(TestDvcGit):
self.dvc.scm.checkout("test", create_new=True)
self.dvc.remove(stages[0].relpath)
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("test")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
@@ -80,7 +80,7 @@ class TestGCBranchesTags(TestDvcGit):
self.dvc.scm.checkout("master")
self.dvc.remove(stages[0].relpath)
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("trash")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
@@ -88,7 +88,7 @@ class TestGCBranchesTags(TestDvcGit):
self.dvc.scm.commit("trash")
self.dvc.remove(stages[0].relpath)
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("master")
stages = self.dvc.add(fname)
self.assertEqual(len(stages), 1)
@@ -138,7 +138,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
# ADD FILE ONLY IN MAIN PROJECT
fname = "only_in_first"
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("only in main repo")
stages = self.dvc.add(fname)
@@ -146,7 +146,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
# ADD FILE IN MAIN PROJECT THAT IS ALSO IN SECOND PROJECT
fname = "in_both"
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("in both repos")
stages = self.dvc.add(fname)
@@ -156,7 +156,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
os.chdir(self.additional_path)
# ADD FILE ONLY IN SECOND PROJECT
fname = "only_in_second"
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("only in additional repo")
stages = self.additional_dvc.add(fname)
@@ -164,7 +164,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
# ADD FILE IN SECOND PROJECT THAT IS ALSO IN MAIN PROJECT
fname = "in_both"
with open(fname, "w+") as fobj:
with open(fname, "w+", encoding="utf-8") as fobj:
fobj.write("in both repos")
stages = self.additional_dvc.add(fname)

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