Compare commits

...
107 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 2a707b706c dvc: bump to 0.34.2 2019-04-04 17:23:42 +03:00
Ruslan Kuprieiev 97b03156d4 Merge pull request #1829 from efiop/1828
dvc: exclude `persist` when computing md5 for the stage
2019-04-04 17:22:36 +03:00
Ruslan Kuprieiev aeff967a13 dvc: exclude persist when computing md5 for the stage
Fixes #1828

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-04 17:12:16 +03:00
Ruslan Kuprieiev 5d41763066 test: requirements: update pytest 2019-04-04 08:27:12 +03:00
Ivan Shcheklein 0fc463094b readme: add missing comma 2019-04-03 21:46:19 -07:00
Ivan Shcheklein a21ee297a1 readme: fix rst italics format 2019-04-03 21:44:40 -07:00
Ivan Shcheklein 2399721d53 readme: emphasize: lfs and makefiles is analogy 2019-04-03 21:43:42 -07:00
Ivan Shcheklein 4ac2ba0989 readme: update description, summary, links 2019-04-03 21:39:20 -07:00
Ivan Shcheklein 55be3ea2f7 readme: use get started link instead of tutorial 2019-04-03 20:57:41 -07:00
Ruslan Kuprieiev 4893266664 dvc: bump to 0.34.1 2019-04-03 22:36:39 +03:00
Ruslan Kuprieiev ce66f97858 Merge pull request #1826 from efiop/pyinstaller
dvc: pyinstaller: fix google.cloud.storage hook
2019-04-03 20:20:08 +03:00
Ruslan Kuprieiev 1c28214728 pyinstaller: hooks: fix google.cloud.storage hook
Fixes #1697

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-03 18:50:15 +03:00
Ruslan Kuprieiev 6ec86e6694 pyinstaller: hook: remove checksumdir hook
That module is not used anymore in dvc.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-04-03 18:47:28 +03:00
Ruslan Kuprieiev a41705a757 dvc: bump to 0.34.0 2019-04-03 00:06:37 +03:00
Shao-Chun Ma 1a75866e72 metrics: automatic type detection (#1809)
Fixes #1553
2019-04-01 21:22:10 +03:00
Ruslan Kuprieiev cbdb18ff8c Merge pull request #1808 from puhoshville/issue-1760
cli: fix overwriting remotes in `dvc remote add`
2019-03-31 23:00:04 +03:00
Ruslan Kuprieiev b21b55e479 Merge pull request #1817 from pared/1807
checkout: reset progress bar on checkout
2019-03-31 22:56:49 +03:00
pared 0f5216cb14 checkout: reset progress bar on checkout 2019-03-31 21:48:21 +02:00
Ruslan Kuprieiev e99a4799f2 Merge pull request #1815 from efiop/status-missing
status: properly handle STATUS_MISSING case
2019-03-31 21:14:14 +03:00
Ruslan Kuprieiev 3794f364d8 status: properly handle STATUS_MISSING case
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-31 21:04:19 +03:00
Ruslan Kuprieiev bf2d3adcf8 Merge pull request #1810 from efiop/1515
system: reflink: workaround SIP error
2019-03-31 20:24:50 +03:00
Ruslan Kuprieiev c956df2cb0 Merge pull request #1813 from efiop/s3-link
dvc: use http remote for downloading dvc images
2019-03-31 20:24:40 +03:00
Ruslan Kuprieiev 0385dec5c0 dvc: use http remote for downloading dvc images
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-31 20:17:26 +03:00
Ruslan Kuprieiev 4904209fd3 Merge pull request #1812 from AmitAronovitch/azure_sas_fails
azure: do not create existing container
2019-03-31 17:38:13 +03:00
Amit Aronovitch 00b81445a9 azure: do not create existing container 2019-03-31 11:39:39 +03:00
Ruslan Kuprieiev 5109d759f1 system: reflink: workaround SIP error
Fixes #1515

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-30 20:53:06 +02:00
Dmitrii Pukhov 67f4c9329e Fix unnecessary raise, ConfirmRemoveError recover 2019-03-30 21:38:47 +09:00
Ruslan Kuprieievandpuhoshville 180b844f46 Update tests/test_remote.py
test_overwrite: fix case of none-zero result on initial stage

Co-Authored-By: puhoshville <[email protected]>
2019-03-30 21:21:37 +09:00
Dmitrii Pukhov 06052a8f6a cli: fix overwriting remotes in dvc remote add
Now dvc remote add requires user confirmation in case of overwriting of
an already existing remote.

Fixes #1760
2019-03-30 14:48:07 +09:00
Ruslan Kuprieiev b3addbd283 Merge pull request #1733 from mroutis/fix-1716
metrics: shows formatted multiline metrics
2019-03-30 03:14:55 +02:00
Ruslan Kuprieiev 374b75f6c7 Merge pull request #1794 from efiop/dvctags
[RFC] introduce `dvc tag`
2019-03-30 02:13:58 +02:00
Mr. Outis 298770efe9 metrics: shows formatted multiline metrics
Close #1716
2019-03-29 16:24:54 -07:00
Ruslan Kuprieiev c645fdd45e dvc: bump to 0.33.1 2019-03-29 21:51:58 +02:00
Ruslan Kuprieiev 278f817ec7 hadoop: use --force when removing data
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 21:31:22 +02:00
Ruslan Kuprieiev 4c5cb7afae checkout: reorganize "git checkout hint" exception
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:44:32 +02:00
Ruslan Kuprieiev c03b1c1693 test: add tests for dvc tag
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:44:11 +02:00
Ruslan Kuprieiev 6e4d74282a dvc: temporarily hide dvc tag
This feature is not yet ready for public use.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:44:11 +02:00
Ruslan Kuprieiev df971ec32d dvc: introduce dvc tag
Fixes #1766

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 13:36:58 +02:00
Ruslan Kuprieiev 76c15ab80d dvc: repo: unify collect() method
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 12:43:19 +02:00
Ruslan Kuprieiev aaa9167a00 dvc: fix remote.checkout() to follow same conventions
Currently `checkout()` accepts `OutputBase*` class as an argument, which
doesn't follow conventions from all other methods, which accept
path_info and/or checksum_info. To fix that, cache for dir info is moved
from output to the underlying remote.

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-29 12:43:19 +02:00
Ruslan Kuprieiev db11175ad9 dvc: bump to 0.33.0 2019-03-29 03:57:30 +02:00
Ivan Shcheklein 4a9188c0b5 Move man doc links to descriptions, fix some help copies (#1805)
* move man doc links to descriptions, fix some help copies

* minor init force message fix

* deal with ugly inheritance in lock base

* remove unnecessary super calls

* add return back to python lock/unlock run functions
2019-03-29 03:56:41 +02:00
Paweł Redzyński 6f9dac280f stage: remove outs upon import (#1796)
* stage: remove outs upon import

* Update dvc/remote/local.py

Co-Authored-By: pared <[email protected]>
2019-03-28 16:45:47 +02:00
Ruslan Kuprieiev c48a5e99be Merge pull request #1795 from ei-grad/fix-test-analytics
tests: fix for globally disabled analytics
2019-03-28 14:45:53 +02:00
Andrew Grigorev e0bf7314a0 tests: fix for globally disabled analytics 2019-03-28 14:54:04 +03:00
Ruslan Kuprieiev f0d3207573 Merge pull request #1793 from pared/1788
repo: fix stages gathering for checkout
2019-03-28 13:47:11 +02:00
pared ace92cb422 repo: fix stages gathering for checkout 2019-03-28 12:13:39 +01:00
Ruslan Kuprieiev a613aafd39 Merge pull request #1787 from ei-grad/ssh-walk-files-error-description
ssh: walk_files error description
2019-03-27 21:00:29 +02:00
Andrew Grigorev 587b645ec9 ssh: walk_files error description 2019-03-27 20:16:24 +03:00
Ruslan Kuprieiev a0e8eff944 Merge pull request #1789 from pared/1786
run: make run respect persist flag
2019-03-27 16:48:38 +02:00
pared bf68fbffec run: make run respect persist flag 2019-03-27 13:28:53 +01:00
Ruslan Kuprieiev 35218a77ab Merge pull request #1781 from puhoshville/fix-1038
cli: add pipeline show --tree
2019-03-26 17:21:33 +02:00
Dmitrii Pukhov 263dee8650 Replace ValueError with DvcException 2019-03-26 22:39:41 +09:00
Ruslan Kuprieiev b791e1f57b Merge pull request #1782 from pared/1740
metrics: rename show, repro: display metrics
2019-03-26 13:10:46 +02:00
Ruslan Kuprieiev 01441f4d12 Merge pull request #1785 from pared/1784
remove: force remove to remove persistent outputs
2019-03-26 13:00:03 +02:00
pared 7b7ecf5bc1 metrics: rename show, repro: fix metrics display problem 2019-03-26 11:56:35 +01:00
pared 70b804307d remove: force remove to remove persistent outputs 2019-03-26 11:41:12 +01:00
Dmitrii Pukhov 5346fc44bc Add treelib dependency to setup.py 2019-03-26 13:51:29 +09:00
Dmitrii Pukhov fba43277e6 cli: add pipeline show --tree
Add option to unfold dependencies into a tree.

Fixes #1038
2019-03-26 01:29:49 +09:00
Ruslan Kuprieiev 2776f2f317 appveyor: debug codecov failure 2019-03-25 18:29:21 +02:00
Paweł Redzyński aa42f76355 Merge pull request #1780 from pared/cmd_run_add_outs_persist_to_obligatory_fields
run: add --outs-persist and --outs-persist-no-cache to obligatory run args
2019-03-25 17:16:30 +01:00
pared af8e9ff17a run: add --outs-persist and --outs-persist-no-cache to obligatory arguments 2019-03-25 17:07:48 +01:00
Ruslan Kuprieiev 49ae03fdc5 Merge pull request #1779 from pared/1763
repo: check whether outputs in graphs do not overlap
2019-03-25 16:19:54 +02:00
pared 2b3107f72c Check whether outputs in graphs do not overlap 2019-03-25 14:19:15 +01:00
Ruslan Kuprieiev e94bc88b26 Merge pull request #1759 from pared/1214
[WIP] run: add --outs-persist and --outs-persist-no-cache options
2019-03-25 11:43:45 +02:00
pared 85b26c2063 run: add --outs-persist option 2019-03-25 09:43:58 +01:00
Peter Fogh ac15a42eac More error information is needed wrt. remote files (#1737)
As described in #1735, I think DVC needs to output more info about remote files,
Thus, I have added this to stage.py.
Additionally, I think that DVC should not crash if the SSH client is trying to remove a fil which already does not exists. This, I have implemented the catch of the FileNotFoundError : [Errno 2] No such file exception.

Fixes #1735
2019-03-23 00:36:46 +02:00
Lee Yi Jie Joel 2ba657c77a Add links to documentation for all commands (#1767) 2019-03-22 20:32:06 +02:00
Andrew Grigorev e67185b75a Merge pull request #1776 from ei-grad/cyclic-import-3
Fix cyclic dependency
2019-03-22 20:10:15 +03:00
Andrew Grigorev 7d79043bbb dvc.scm.scm_context -> dvc.repo.scm_context.scm_context
The `dvc/scm/__init__.py:scm_context` works with `Repo` objects and
is used mostly in `dvc/repo/...`, so it is better to put it in
`dvc/repo/scm_context.py`.

Fix #1769
2019-03-22 19:53:41 +03:00
Andrew Grigorev 6fbdf1b5b0 dvc.repo.tree -> dvc.scm.tree
Tree objects are used as `Repo().tree`, so it seemed logical to put them
inside `dvc/repo/tree.py`. But what is more important - `dvc.repo.Repo`
depends on `dvc.scm` and it is not convinient to use `dvc.repo.tree` in
`dvc.scm.git`.

Fix #1769
2019-03-22 19:53:41 +03:00
Ruslan Kuprieiev 402eb2da01 travis: debug codecov failure 2019-03-22 15:50:30 +02:00
Ruslan Kuprieiev d27e8ded61 Merge pull request #1749 from mroutis/fix-1747
cache: provide at least one value in parser
2019-03-21 22:47:19 +02:00
Ruslan Kuprieiev bc65805749 Merge pull request #1709 from ei-grad/brancher-refactor
Brancher refactor
2019-03-21 22:12:56 +02:00
Mr. Outis 1df5dea351 cache: provide at least one value in parser
Fix #1747
2019-03-21 12:28:46 -07:00
Ruslan Kuprieiev 0897e8e9ba Merge pull request #1764 from mroutis/update-completion-scripts
scripts: update completion for zsh and bash
2019-03-21 21:18:56 +02:00
Andrew Grigorev 9219398d83 tests: fix #1754 2019-03-21 19:47:42 +03:00
Andrew Grigorev 63dd3f2c25 tests: fix get_gitignore_content
readlines() returns entries with \n on the end
2019-03-21 19:47:42 +03:00
Andrew Grigorev 2ef3a7c7c0 tests: windows has different linesep 2019-03-21 19:47:42 +03:00
Andrew Grigorev 8fa968cf33 collect: nx.dfs_postorder_nodes yeilds the passed node 2019-03-21 19:47:42 +03:00
Andrew Grigorev 16f74f6894 tests: test_metrics missing .dvc files in commit 2019-03-21 19:47:42 +03:00
Andrew Grigorev 8c8b585de9 tests: test_install commit and clean
Uncommited files wouldn't be accessible as git objects.

It needs `git clean -fd` to amend "Working Tree" metrics collection
which took its part when the git tree is dirty.
2019-03-21 19:47:42 +03:00
Andrew Grigorev 9e6c52079b Abstract access to file objects
Abstract access to filesystem and its implementation to access files and
particulary Stage objects in different git branches without `git
checkout` execution.
2019-03-21 19:47:42 +03:00
Mr. Outis ce65771596 scripts: update completion for zsh and bash
Fix #1732
2019-03-21 09:28:54 -07:00
Andrew Grigorev e782344a98 Move dvc/scm/git.py to dvc/scm/git/__init__.py 2019-03-21 18:03:06 +03:00
Paweł Redzyński c65495e15f RFC data cloud: declutter test (#1761) 2019-03-21 16:48:28 +02:00
Ivan Shcheklein 9e944c3c61 Merge pull request #1757 from J0/patch-1
Fix typo in repro.py
2019-03-20 16:13:54 -07:00
Lee Yi Jie Joel e0a1d7803d Fix typo in repro.py 2019-03-20 16:12:15 -07:00
Paweł Redzyński 5a736cb79a remote local, repo: use walk_files instead of os.walk (#1750) 2019-03-20 18:45:21 +02:00
Ruslan Kuprieiev 24072c8083 daemon: don't dump env into debug log 2019-03-20 16:07:33 +02:00
Paweł Redzyński ee16409495 checkout: progress bar (#1713) 2019-03-20 13:39:10 +02:00
Dmitry Petrov 70bc101571 [WIP] Pkg install (#1742)
* Download Git package

* Extract tmp git repo & fetch data

* Handle fetch\cloud errors and rename select option

* Modify gitignore files when deploy a package

* More tests for git ignore file. Fix an old issue.

* Raise and exception when .gitignor not in a file subdir

* Existing tests fixes

* Basic test for packages

* Comment out pkg tests

* Codeclimate improvements

* Reformatting

* Enable pkg tests

* Fix tests no Windows

* Another old bug fix

* Fix encoding issue on Windows

* Another windows fix: apply compat.str magic to file write

* Back to posix path in gitignore and apply compat.str magic to tests

* gitignorepath: OS specifix path to posix

* Forgotten assignment

* misc: cleanup small issues

Signed-off-by: Ruslan Kuprieiev <[email protected]>

* cli: pkg: hide from help

Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-20 04:09:12 +02:00
Ruslan Kuprieiev a691985576 cli: daemon: hide from help
Fixes #1657
2019-03-20 03:30:55 +02:00
Ruslan Kuprieiev 1d08ed8834 repo: unprotect: remove unused move 2019-03-19 20:03:01 +02:00
Ruslan Kuprieiev 7cd618ce5d repo: unprotect: use os.rename() instead of move()
Follow up for #1752
2019-03-19 19:42:25 +02:00
Ruslan Kuprieiev 90e41de5f7 Merge pull request #1752 from ei-grad/unprotect-improvement
unprotect: copy should preceed the move
2019-03-19 19:36:34 +02:00
Andrew Grigorev beb32b387e unprotect: copy should preceed the move
It is better to make a file copy before moving it, since move is
a nearly-atomic operation and copy is a long one.

Also the biggest problem here is that if some application would access
the file during the process of `copyfile` then it would get the part of
file.
2019-03-19 20:14:42 +03:00
Ruslan Kuprieiev e1346a8a36 Merge pull request #1741 from shcheklein/migrate-to-pytest
Migrate to pytest
2019-03-18 11:25:07 +02:00
Ivan Shcheklein 08036b445b set timeouts method to thread to avoid problems in parallel run 2019-03-17 20:11:01 -07:00
Ivan Shcheklein f085e01717 try to fix progess bar test in a parallel pytest environment 2019-03-17 19:33:09 -07:00
Ivan Shcheklein 75421f0f21 migrate to pytest 2019-03-17 14:49:49 -07:00
Ruslan Kuprieiev a54eaeb59e Merge pull request #1734 from mroutis/fix-1698
cli: Use correct color codes on Windows
2019-03-17 18:37:45 +02:00
Ruslan Kuprieiev 7a5ff38198 Merge pull request #1738 from PeterFogh/patch-1
Added `--exe` argument to nosetest default
2019-03-17 15:50:46 +02:00
Ruslan Kuprieiev ee51ccab4a test: reword --exec explanation
Signed-off-by: Ruslan Kuprieiev <[email protected]>
2019-03-17 15:44:02 +02:00
Peter Fogh d87ceb4e46 Added --exe argument to nosetest default
See https://github.com/iterative/dvc/issues/1736#issuecomment-473665122
2019-03-17 14:34:18 +01:00
Ruslan Kuprieiev fc646882ad deploy: use xcode 8.3 for mac 2019-03-17 14:38:51 +02:00
Mr. Outis e4bc59ca03 cli: Use correct color codes on Windows
Fix #1698
2019-03-16 17:51:28 -06:00
139 changed files with 3590 additions and 711 deletions
+1
View File
@@ -75,6 +75,7 @@ test_script:
- python -mtests
after_test:
- if "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" pip install codecov
- if "%APPVEYOR_PULL_REQUEST_NUMBER%"=="" codecov
- if "%APPVEYOR_REPO_TAG%"=="true" python -m dvc pull
- if "%APPVEYOR_REPO_TAG%"=="true" .\scripts\build_windows.cmd
+1 -1
View File
@@ -1,4 +1,4 @@
['remote "dvc"']
url = s3://dvc-share/dvc/
url = https://dvc.org/s3/dvc
[core]
remote = dvc
+3
View File
@@ -27,3 +27,6 @@ innosetup/config.ini
dvc/version.py
*.swp
pip-wheel-metadata/
.vscode/
+39 -19
View File
@@ -6,7 +6,7 @@
• `Docs <https://dvc.org/doc>`_
• `Twitter <https://twitter.com/iterativeai>`_
• `Chat (Community & Support) <https://dvc.org/chat>`_
• `Tutorial <https://dvc.org/doc/tutorial>`_
• `Tutorial <https://dvc.org/doc/get-started>`_
• `Mailing List <https://sweedom.us10.list-manage.com/subscribe/post?u=a08bf93caae4063c4e6a351f6&id=24c0ecc49a>`_
.. image:: https://travis-ci.com/iterative/dvc.svg?branch=master
@@ -28,17 +28,18 @@
|
**Data Science Version Control** or **DVC** is an **open-source** tool for data science and
machine learning projects. With a simple and flexible Git-like architecture and interface it
helps data scientists:
machine learning projects. Key features:
#. manage **machine learning models** - versioning, including data sets and transformations (scripts) that were
used to generate models;
#. simple **command line** Git-like exprerience. Does not require installing and maintaining
any databases. Does not depend on any proprietary online services;
#. make projects **reproducible**;
#. it manages and versions **datasets** and **machine learning models**. Data is saved in
S3, Google cloud, Azure, SSH server, HDFS or even local HDD RAID;
#. make projects **shareable**;
#. it makes projects **reproducible** and **shareable**, it helps answering question how
the model was build;
#. manage experiments with branching and **metrics** tracking;
#. it helps manage experiments with Git tags or branches and **metrics** tracking;
It aims to replace tools like Excel and Docs that are being commonly used as a knowledge repo and
a ledger for the team, ad-hoc scripts to track and move deploy different model versions, ad-hoc
@@ -50,14 +51,30 @@ data file suffixes and prefixes.
How DVC works
=============
DVC is compatible with Git for storing code and the dependency graph (DAG), but not data files cache.
To store and share data files cache DVC supports remotes - any cloud (S3, Azure, Google Cloud, etc) or any on-premise
network storage (via SSH, for example).
We encourage you to read our `Get Started <https://dvc.org/doc/get-started>`_ to better understand what DVC
is and how does it fit your scenarios.
The easiest (but not perfect!) *analogy* to describe it: DVC is Git (or Git-lfs to be precise) + ``makefiles``
made right and tailored specifically for ML and Data Science scenarios.
#. ``Git/Git-lfs`` part - DVC helps you storing and sharing data artifacts, models. It connects them with your
Git repository.
#. ``Makefiles`` part - DVC describes how one data or model artifact was build from another data.
DVC usually runs along with Git. Git is used as usual to store and version code and DVC meta-files. DVC helps
to store data and model files seamlessly out of Git while preserving almost the same user experience as if they
were stored in Git itself. To store and share data files cache DVC supports remotes - any cloud (S3, Azure,
Google Cloud, etc) or any on-premise network storage (via SSH, for example). It does not require
.. image:: https://dvc.org/static/img/flow.gif
:target: https://dvc.org/static/img/flow.gif
:alt: how_dvc_works
DVC pipelines (aka computational graph) feature connects code and data together. In a very explicit way you can
specify, run, and save information that a certain command with certain dependencies needs to be run to produce
a model. See the quick start section below or check `Get Started <https://dvc.org/doc/get-started>`_ tutorial to
learn more.
Quick start
===========
@@ -66,7 +83,7 @@ Please read `Get Started <https://dvc.org/doc/get-started>`_ for the full versio
+-----------------------------------+-------------------------------------------------------------------+
| Step | Command |
+===================================+===================================================================+
| Track code and data together | | ``$ git add train.py`` |
| Track data | | ``$ git add train.py`` |
| | | ``$ dvc add images.zip`` |
+-----------------------------------+-------------------------------------------------------------------+
| Connect code and data by commands | | ``$ dvc run -d images.zip -o images/ unzip -q images.zip`` |
@@ -79,27 +96,30 @@ Please read `Get Started <https://dvc.org/doc/get-started>`_ for the full versio
| | | ``$ git commit -m 'The baseline model'`` |
| | | ``$ git push`` |
+-----------------------------------+-------------------------------------------------------------------+
| Share data and ML models | | ``$ dvc remote add myremote s3://mybucket/image_cnn`` |
| | | ``$ dvc config core.remote myremote`` |
| Share data and ML models | | ``$ dvc remote add myremote -d s3://mybucket/image_cnn`` |
| | | ``$ dvc push`` |
+-----------------------------------+-------------------------------------------------------------------+
Installation
============
There are three options to install DVC: ``pip``, Homebrew, or an OS-specific package:
Read this `instruction <https://dvc.org/doc/get-started/install>`_ to get more details. There are three
options to install DVC: ``pip``, Homebrew, or an OS-specific package:
pip (PyPI)
----------
Stable
^^^^^^
.. code-block:: bash
pip install dvc
Development
^^^^^^^^^^^
Depending on the remote storage type you plan to use to keep and share your data, you might need to specify
one of the optional dependencies: ``s3``, ``gs``, ``azure``, ``ssh``. Or ``all_remotes`` to include them all.
The command should look like this: ``pip install dvc[s3]`` - it installs the ``boto3`` library along with
DVC to support the AWS S3 storage.
To install the development version, run:
.. code-block:: bash
pip install git+git://github.com/iterative/dvc
+2 -1
View File
@@ -10,7 +10,7 @@ import os
import warnings
VERSION_BASE = "0.32.1"
VERSION_BASE = "0.34.2"
__version__ = VERSION_BASE
PACKAGEPATH = os.path.abspath(os.path.dirname(__file__))
@@ -57,6 +57,7 @@ if os.path.exists(os.path.join(HOMEPATH, "setup.py")):
if (
os.getenv("APPVEYOR_REPO_TAG", "").lower() != "true"
and os.getenv("TRAVIS_TAG", "") == ""
and os.getenv("DVC_TEST", "").lower() != "true"
):
__version__ = _update_version_file()
else: # pragma: no cover
+9 -3
View File
@@ -201,6 +201,13 @@ class Analytics(object):
json.dump(self.info, fobj)
return fobj.name
@staticmethod
def _is_enabled_config(config):
from dvc.config import Config
core = config.config.get(Config.SECTION_CORE, {})
return core.get(Config.SECTION_CORE_ANALYTICS, True)
@staticmethod
def _is_enabled(cmd=None):
from dvc.config import Config
@@ -226,10 +233,9 @@ class Analytics(object):
config = cmd.config
assert config is not None
core = config.config.get(Config.SECTION_CORE, {})
enabled = core.get(Config.SECTION_CORE_ANALYTICS, True)
enabled = Analytics._is_enabled_config(config)
logger.debug(
"Analytics is {}abled.".format("en" if enabled else "dis")
"Analytics is {}.".format("enabled" if enabled else "disabled")
)
return enabled
+4
View File
@@ -9,6 +9,7 @@ import argparse
import dvc.logger as logger
from dvc.command.base import fix_subparsers
import dvc.command.init as init
import dvc.command.pkg as pkg
import dvc.command.destroy as destroy
import dvc.command.remove as remove
import dvc.command.move as move
@@ -30,12 +31,14 @@ import dvc.command.lock as lock
import dvc.command.pipeline as pipeline
import dvc.command.daemon as daemon
import dvc.command.commit as commit
import dvc.command.tag as tag
from dvc.exceptions import DvcParserError
from dvc import VERSION
COMMANDS = [
init,
pkg,
destroy,
add,
remove,
@@ -58,6 +61,7 @@ COMMANDS = [
pipeline,
daemon,
commit,
tag,
]
+10 -3
View File
@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdAdd(CmdBase):
@@ -26,9 +28,14 @@ class CmdAdd(CmdBase):
def add_parser(subparsers, parent_parser):
ADD_HELP = "Add files/directories to dvc."
ADD_HELP = "Take data files or directories under DVC control."
add_parser = subparsers.add_parser(
"add", parents=[parent_parser], description=ADD_HELP, help=ADD_HELP
"add",
parents=[parent_parser],
description=append_doc_link(ADD_HELP, "add"),
help=ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
add_parser.add_argument(
"-R",
+15
View File
@@ -1,5 +1,7 @@
from __future__ import unicode_literals
import colorama
import dvc.logger as logger
@@ -18,6 +20,19 @@ def fix_subparsers(subparsers):
subparsers.dest = "cmd"
def append_doc_link(help_message, path):
if not path:
return help_message
doc_base = "https://man.dvc.org/"
return "{message}\ndocumentation: {blue}{base}{path}{nc}".format(
message=help_message,
base=doc_base,
path=path,
blue=colorama.Fore.CYAN,
nc=colorama.Fore.RESET,
)
class CmdBase(object):
def __init__(self, args):
from dvc.repo import Repo
+7 -4
View File
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import argparse
from dvc.command.base import fix_subparsers
from dvc.command.base import fix_subparsers, append_doc_link
from dvc.command.remote import CmdRemoteAdd
from dvc.command.config import CmdConfig
@@ -21,11 +21,13 @@ def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
CACHE_HELP = "Manage cache settings."
cache_parser = subparsers.add_parser(
"cache",
parents=[parent_parser],
description=CACHE_HELP,
description=append_doc_link(CACHE_HELP, "cache"),
help=CACHE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_subparsers = cache_parser.add_subparsers(
@@ -38,11 +40,13 @@ def add_parser(subparsers, parent_parser):
add_help=False, parents=[parent_config_parser]
)
CACHE_DIR_HELP = "Configure cache directory location."
cache_dir_parser = cache_subparsers.add_parser(
"dir",
parents=[parent_cache_config_parser],
description=CACHE_DIR_HELP,
description=append_doc_link(CACHE_HELP, "cache-dir"),
help=CACHE_DIR_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
cache_dir_parser.add_argument(
"-u",
@@ -53,7 +57,6 @@ def add_parser(subparsers, parent_parser):
)
cache_dir_parser.add_argument(
"value",
nargs="?",
default=None,
help="Path to cache directory. Relative paths are resolved relative "
"to the current directory and saved to config relative to the "
+14 -2
View File
@@ -1,6 +1,8 @@
from __future__ import unicode_literals
from dvc.command.base import CmdBase
import argparse
from dvc.command.base import CmdBase, append_doc_link
class CmdCheckout(CmdBase):
@@ -13,17 +15,20 @@ class CmdCheckout(CmdBase):
target=target,
with_deps=self.args.with_deps,
force=self.args.force,
recursive=self.args.recursive,
)
return 0
def add_parser(subparsers, parent_parser):
CHECKOUT_HELP = "Checkout data files from cache."
checkout_parser = subparsers.add_parser(
"checkout",
parents=[parent_parser],
description=CHECKOUT_HELP,
description=append_doc_link(CHECKOUT_HELP, "checkout"),
help=CHECKOUT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
checkout_parser.add_argument(
"-d",
@@ -39,5 +44,12 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Do not prompt when removing working directory files.",
)
checkout_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Checkout all subdirectories of the specified directory.",
)
checkout_parser.add_argument("targets", nargs="*", help="DVC files.")
checkout_parser.set_defaults(func=CmdCheckout)
+7 -3
View File
@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdCommit(CmdBase):
@@ -29,12 +31,14 @@ class CmdCommit(CmdBase):
def add_parser(subparsers, parent_parser):
COMMIT_HELP = "Record changes to the repository."
COMMIT_HELP = "Save changed data to cache and update DVC files."
commit_parser = subparsers.add_parser(
"commit",
parents=[parent_parser],
description=COMMIT_HELP,
description=append_doc_link(COMMIT_HELP, "commit"),
help=COMMIT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
commit_parser.add_argument(
"-f",
+4 -2
View File
@@ -4,7 +4,7 @@ import os
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.config import Config
from dvc.exceptions import DvcException
@@ -99,11 +99,13 @@ parent_config_parser.add_argument(
def add_parser(subparsers, parent_parser):
CONFIG_HELP = "Get or set config options."
config_parser = subparsers.add_parser(
"config",
parents=[parent_config_parser, parent_parser],
description=CONFIG_HELP,
description=append_doc_link(CONFIG_HELP, "config"),
help=CONFIG_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
config_parser.add_argument(
"-u",
+1 -1
View File
@@ -43,7 +43,7 @@ def add_parser(subparsers, parent_parser):
"daemon",
parents=[parent_parser],
description=DAEMON_HELP,
help=DAEMON_HELP,
add_help=False,
)
daemon_subparsers = daemon_parser.add_subparsers(
+19 -9
View File
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdDataBase(CmdBase):
@@ -120,12 +120,14 @@ def add_parser(subparsers, _parent_parser):
from dvc.command.status import CmdDataStatus
# Pull
PULL_HELP = "Pull data files from the cloud."
PULL_HELP = "Pull data files from a DVC remote storage."
pull_parser = subparsers.add_parser(
"pull",
parents=[shared_parent_parser()],
description=PULL_HELP,
description=append_doc_link(PULL_HELP, "pull"),
help=PULL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pull_parser.add_argument(
"-r", "--remote", help="Remote repository to pull from."
@@ -168,12 +170,14 @@ def add_parser(subparsers, _parent_parser):
pull_parser.set_defaults(func=CmdDataPull)
# Push
PUSH_HELP = "Push data files to the cloud."
PUSH_HELP = "Push data files to a DVC remote storage."
push_parser = subparsers.add_parser(
"push",
parents=[shared_parent_parser()],
description=PUSH_HELP,
description=append_doc_link(PUSH_HELP, "push"),
help=PUSH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
push_parser.add_argument(
"-r", "--remote", help="Remote repository to push to."
@@ -209,12 +213,14 @@ def add_parser(subparsers, _parent_parser):
push_parser.set_defaults(func=CmdDataPush)
# Fetch
FETCH_HELP = "Fetch data files from the cloud."
FETCH_HELP = "Fetch data files from a DVC remote storage."
fetch_parser = subparsers.add_parser(
"fetch",
parents=[shared_parent_parser()],
description=FETCH_HELP,
description=append_doc_link(FETCH_HELP, "fetch"),
help=FETCH_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
fetch_parser.add_argument(
"-r", "--remote", help="Remote repository to fetch from."
@@ -250,13 +256,17 @@ def add_parser(subparsers, _parent_parser):
fetch_parser.set_defaults(func=CmdDataFetch)
# Status
STATUS_HELP = "Show the repo status."
STATUS_HELP = (
"Show changed stages, compare local cache and a remote storage."
)
status_parser = subparsers.add_parser(
"status",
parents=[shared_parent_parser()],
description=STATUS_HELP,
description=append_doc_link(STATUS_HELP, "status"),
help=STATUS_HELP,
conflict_handler="resolve",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
status_parser.add_argument(
"-q",
+7 -6
View File
@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import argparse
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
@@ -30,15 +32,14 @@ class CmdDestroy(CmdBase):
def add_parser(subparsers, parent_parser):
DESTROY_HELP = (
"Destroy dvc. Will remove all repo's information, "
"data files and cache."
)
DESTROY_HELP = "Remove DVC files, local DVC config and data cache."
destroy_parser = subparsers.add_parser(
"destroy",
parents=[parent_parser],
description=DESTROY_HELP,
description=append_doc_link(DESTROY_HELP, "destroy"),
help=DESTROY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
destroy_parser.add_argument(
"-f",
+12 -7
View File
@@ -1,10 +1,11 @@
from __future__ import unicode_literals
import argparse
import os
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdGC(CmdBase):
@@ -46,13 +47,17 @@ class CmdGC(CmdBase):
def add_parser(subparsers, parent_parser):
GC_HELP = (
"Collect garbage from your DVC cache. \n"
"Deletes all files in the cache which are not in use by the "
"specified git references (defaults to just HEAD)."
GC_HELP = "Collect unused data from DVC cache or a remote storage."
GC_DESCRIPTION = (
"Deletes all files in the cache or a remote which are not in\n"
"use by the specified git references (defaults to just HEAD)."
)
gc_parser = subparsers.add_parser(
"gc", parents=[parent_parser], description=GC_HELP, help=GC_HELP
"gc",
parents=[parent_parser],
description=append_doc_link(GC_DESCRIPTION, "gc"),
help=GC_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
gc_parser.add_argument(
"-a",
@@ -76,7 +81,7 @@ def add_parser(subparsers, parent_parser):
help="Collect garbage in remote repository.",
)
gc_parser.add_argument(
"-r", "--remote", help="Remote repository to collect garbage in."
"-r", "--remote", help="Remote storage to collect garbage in."
)
gc_parser.add_argument(
"-f",
+18 -14
View File
@@ -1,11 +1,12 @@
from __future__ import unicode_literals
import argparse
import os
import dvc.logger as logger
from dvc.utils.compat import urlparse
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdImport(CmdBase):
@@ -28,25 +29,26 @@ class CmdImport(CmdBase):
def add_parser(subparsers, parent_parser):
IMPORT_HELP = "Import files from URL."
IMPORT_HELP = "Download or copy files from URL and take under DVC control."
import_parser = subparsers.add_parser(
"import",
parents=[parent_parser],
description=IMPORT_HELP,
description=append_doc_link(IMPORT_HELP, "import"),
help=IMPORT_HELP,
formatter_class=argparse.RawTextHelpFormatter,
)
import_parser.add_argument(
"url",
help="URL. Supported urls: "
"'/path/to/file', "
"'C:\\\\path\\to\\file', "
"'https://example.com/path/to/file', "
"'s3://bucket/path/to/file', "
"'gs://bucket/path/to/file', "
"'hdfs://example.com/path/to/file', "
"'ssh://example.com:/path/to/file', "
"'remote://myremote/path/to/file'(see "
"`dvc remote` commands). ",
help="Supported urls:\n"
"/path/to/file\n"
"C:\\\\path\\to\\file\n"
"https://example.com/path/to/file\n"
"s3://bucket/path/to/file\n"
"gs://bucket/path/to/file\n"
"hdfs://example.com/path/to/file\n"
"ssh://example.com:/path/to/file\n"
"remote://myremote/path/to/file (see `dvc remote`)",
)
import_parser.add_argument(
"--resume",
@@ -54,5 +56,7 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Resume previously started download.",
)
import_parser.add_argument("out", nargs="?", help="Output.")
import_parser.add_argument(
"out", nargs="?", help="Destination path to put files to."
)
import_parser.set_defaults(func=CmdImport)
+17 -4
View File
@@ -1,6 +1,9 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import append_doc_link
class CmdInit(object):
@@ -24,11 +27,18 @@ class CmdInit(object):
def add_parser(subparsers, parent_parser):
"""Setup parser for `dvc init`."""
INIT_HELP = (
"Initialize dvc over a directory " "(should already be a git dir)."
INIT_HELP = "Initialize DVC in the current directory."
INIT_DESCRIPTION = (
"Initialize DVC in the current directory. Expects directory\n"
"to be a Git repository unless --no-scm option is specified."
)
init_parser = subparsers.add_parser(
"init", parents=[parent_parser], description=INIT_HELP, help=INIT_HELP
"init",
parents=[parent_parser],
description=append_doc_link(INIT_DESCRIPTION, "init"),
help=INIT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
init_parser.add_argument(
"--no-scm",
@@ -42,6 +52,9 @@ def add_parser(subparsers, parent_parser):
"--force",
action="store_true",
default=False,
help="Overwrite '.dvc' if it exists. Will remove all local cache.",
help=(
"Overwrite existing '.dvc' directory. "
"This operation removes local cache."
),
)
init_parser.set_defaults(func=CmdInit)
+6 -3
View File
@@ -1,7 +1,9 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdInstall(CmdBase):
@@ -15,11 +17,12 @@ class CmdInstall(CmdBase):
def add_parser(subparsers, parent_parser):
INSTALL_HELP = "Install dvc hooks into the repository."
INSTALL_HELP = "Install DVC git hooks into the repository."
install_parser = subparsers.add_parser(
"install",
parents=[parent_parser],
description=INSTALL_HELP,
description=append_doc_link(INSTALL_HELP, "install"),
help=INSTALL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
install_parser.set_defaults(func=CmdInstall)
+13 -6
View File
@@ -1,12 +1,14 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdLockBase(CmdBase):
def run(self, unlock=False):
def _run(self, unlock):
for target in self.args.targets:
try:
self.repo.lock_stage(target, unlock=unlock)
@@ -23,18 +25,22 @@ class CmdLockBase(CmdBase):
class CmdLock(CmdLockBase):
def run(self):
return super(CmdLock, self).run(False)
return self._run(False)
class CmdUnlock(CmdLockBase):
def run(self):
return super(CmdUnlock, self).run(True)
return self._run(True)
def add_parser(subparsers, parent_parser):
LOCK_HELP = "Lock DVC file."
lock_parser = subparsers.add_parser(
"lock", parents=[parent_parser], description=LOCK_HELP, help=LOCK_HELP
"lock",
parents=[parent_parser],
description=append_doc_link(LOCK_HELP, "lock"),
help=LOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
lock_parser.add_argument("targets", nargs="+", help="DVC files.")
lock_parser.set_defaults(func=CmdLock)
@@ -43,8 +49,9 @@ def add_parser(subparsers, parent_parser):
unlock_parser = subparsers.add_parser(
"unlock",
parents=[parent_parser],
description=UNLOCK_HELP,
description=append_doc_link(UNLOCK_HELP, "unlock"),
help=UNLOCK_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unlock_parser.add_argument("targets", nargs="+", help="DVC files.")
unlock_parser.set_defaults(func=CmdUnlock)
+46 -20
View File
@@ -1,19 +1,36 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
def show_metrics(metrics, all_branches=False, all_tags=False):
"""
Args:
metrics (list): Where each element is either a `list`
if an xpath was specified, otherwise a `str`
"""
for branch, val in metrics.items():
if all_branches or all_tags:
logger.info("{branch}:".format(branch=branch))
for fname, metric in val.items():
lines = metric if type(metric) is list else metric.splitlines()
if len(lines) > 1:
logger.info("\t{fname}:".format(fname=fname))
for line in lines:
logger.info("\t\t{content}".format(content=line))
else:
logger.info("\t{}: {}".format(fname, metric))
class CmdMetricsShow(CmdBase):
def _show(self, metrics):
for branch, val in metrics.items():
if self.args.all_branches or self.args.all_tags:
logger.info("{}:".format(branch))
for fname, metric in val.items():
logger.info("\t{}: {}".format(fname, metric))
def run(self):
typ = self.args.type
xpath = self.args.xpath
@@ -27,7 +44,7 @@ class CmdMetricsShow(CmdBase):
recursive=self.args.recursive,
)
self._show(metrics)
show_metrics(metrics, self.args.all_branches, self.args.all_tags)
except DvcException:
logger.error("failed to show metrics")
return 1
@@ -75,15 +92,14 @@ class CmdMetricsRemove(CmdBase):
def add_parser(subparsers, parent_parser):
METRICS_HELP = (
"A set of commands to add, manage, collect and display project "
"metrics."
)
METRICS_HELP = "Commands to add, manage, collect and display metrics."
metrics_parser = subparsers.add_parser(
"metrics",
parents=[parent_parser],
description=METRICS_HELP,
description=append_doc_link(METRICS_HELP, "metrics"),
help=METRICS_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_subparsers = metrics_parser.add_subparsers(
@@ -97,14 +113,21 @@ def add_parser(subparsers, parent_parser):
metrics_show_parser = metrics_subparsers.add_parser(
"show",
parents=[parent_parser],
description=METRICS_SHOW_HELP,
description=append_doc_link(METRICS_SHOW_HELP, "metrics-show"),
help=METRICS_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_show_parser.add_argument(
"path", nargs="?", help="Path to a metric file or a directory."
)
metrics_show_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
"-t",
"--type",
help=(
"Type of metrics (json/tsv/htsv/csv/hcsv). "
"It can be detected by the file extension automatically. "
"Unsupported types will be treated as raw."
),
)
metrics_show_parser.add_argument(
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
@@ -139,8 +162,9 @@ def add_parser(subparsers, parent_parser):
metrics_add_parser = metrics_subparsers.add_parser(
"add",
parents=[parent_parser],
description=METRICS_ADD_HELP,
description=append_doc_link(METRICS_ADD_HELP, "metrics-add"),
help=METRICS_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_add_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
@@ -155,8 +179,9 @@ def add_parser(subparsers, parent_parser):
metrics_modify_parser = metrics_subparsers.add_parser(
"modify",
parents=[parent_parser],
description=METRICS_MODIFY_HELP,
description=append_doc_link(METRICS_MODIFY_HELP, "metrics-modify"),
help=METRICS_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_modify_parser.add_argument(
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
@@ -171,8 +196,9 @@ def add_parser(subparsers, parent_parser):
metrics_remove_parser = metrics_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=METRICS_REMOVE_HELP,
description=append_doc_link(METRICS_REMOVE_HELP, "metrics-remove"),
help=METRICS_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
metrics_remove_parser.add_argument("path", help="Path to a metric file.")
metrics_remove_parser.set_defaults(func=CmdMetricsRemove)
+14 -7
View File
@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdMove(CmdBase):
@@ -19,14 +21,19 @@ class CmdMove(CmdBase):
def add_parser(subparsers, parent_parser):
description = (
"Rename or move a data file or a directory that "
"is under DVC control. It renames and modifies "
"the corresponding DVC file to reflect the changes."
MOVE_HELP = "Rename or move a DVC controlled data file or a directory."
MOVE_DESCRIPTION = (
"Rename or move a DVC controlled data file or a directory.\n"
"It renames and modifies the corresponding DVC file to reflect the"
" changes."
)
help = "Rename or move a DVC controlled data file or a directory."
move_parser = subparsers.add_parser(
"move", parents=[parent_parser], description=description, help=help
"move",
parents=[parent_parser],
description=append_doc_link(MOVE_DESCRIPTION, "move"),
help=MOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
move_parser.add_argument(
"src", help="Source path to a data file or directory."
+47 -8
View File
@@ -1,12 +1,14 @@
from __future__ import unicode_literals
import argparse
from dvc.utils.compat import str
import os
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
class CmdPipelineShow(CmdBase):
@@ -71,23 +73,47 @@ class CmdPipelineShow(CmdBase):
else:
edges.append((from_stage.relpath, to_stage.relpath))
return nodes, edges
return nodes, edges, networkx.is_tree(G)
def _show_ascii(self, target, commands, outs):
from dvc.dagascii import draw
nodes, edges = self.__build_graph(target, commands, outs)
nodes, edges, _ = self.__build_graph(target, commands, outs)
if not nodes:
return
draw(nodes, edges)
def _show_dependencies_tree(self, target, commands, outs):
from treelib import Tree
nodes, edges, is_tree = self.__build_graph(target, commands, outs)
if not nodes:
return
if not is_tree:
raise DvcException(
"DAG is not a tree, can not print it in tree-structure way, "
"please use --ascii instead"
)
tree = Tree()
tree.create_node(target, target) # Root node
observe_list = [target]
while len(observe_list) > 0:
current_root = observe_list[0]
for edge in edges:
if edge[0] == current_root:
tree.create_node(edge[1], edge[1], parent=current_root)
observe_list.append(edge[1])
observe_list.pop(0)
tree.show()
def __write_dot(self, target, commands, outs, filename):
import networkx
from networkx.drawing.nx_pydot import write_dot
_, edges = self.__build_graph(target, commands, outs)
_, edges, _ = self.__build_graph(target, commands, outs)
edges = [edge[::-1] for edge in edges]
simple_g = networkx.DiGraph()
@@ -111,6 +137,10 @@ class CmdPipelineShow(CmdBase):
self.args.outs,
self.args.dot,
)
elif self.args.tree:
self._show_dependencies_tree(
target, self.args.commands, self.args.outs
)
else:
self._show(target, self.args.commands, self.args.outs)
except DvcException:
@@ -137,12 +167,13 @@ class CmdPipelineList(CmdBase):
def add_parser(subparsers, parent_parser):
PIPELINE_HELP = "Manage pipeline."
PIPELINE_HELP = "Manage pipelines."
pipeline_parser = subparsers.add_parser(
"pipeline",
parents=[parent_parser],
description=PIPELINE_HELP,
description=append_doc_link(PIPELINE_HELP, "pipeline"),
help=PIPELINE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_subparsers = pipeline_parser.add_subparsers(
@@ -156,8 +187,9 @@ def add_parser(subparsers, parent_parser):
pipeline_show_parser = pipeline_subparsers.add_parser(
"show",
parents=[parent_parser],
description=PIPELINE_SHOW_HELP,
description=append_doc_link(PIPELINE_SHOW_HELP, "pipeline-show"),
help=PIPELINE_SHOW_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_show_group = pipeline_show_parser.add_mutually_exclusive_group()
pipeline_show_group.add_argument(
@@ -183,6 +215,12 @@ def add_parser(subparsers, parent_parser):
pipeline_show_parser.add_argument(
"--dot", help="Write DAG in .dot format."
)
pipeline_show_parser.add_argument(
"--tree",
action="store_true",
default=False,
help="Output DAG as Dependencies Tree.",
)
pipeline_show_parser.add_argument(
"targets", nargs="*", help="DVC files. 'Dvcfile' by default."
)
@@ -192,7 +230,8 @@ def add_parser(subparsers, parent_parser):
pipeline_list_parser = pipeline_subparsers.add_parser(
"list",
parents=[parent_parser],
description=PIPELINE_LIST_HELP,
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline-list"),
help=PIPELINE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pipeline_list_parser.set_defaults(func=CmdPipelineList)
+89
View File
@@ -0,0 +1,89 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
from dvc.repo.pkg import PackageParams
class CmdPkg(CmdBase):
def run(self, unlock=False):
try:
pkg_params = PackageParams(
self.args.address,
self.args.target_dir,
self.args.select,
self.args.file,
)
return self.repo.install_pkg(pkg_params)
except DvcException:
logger.error(
"failed to install package '{}'".format(self.args.address)
)
return 1
pass
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
PKG_HELP = "Manage DVC packages."
pkg_parser = subparsers.add_parser(
"pkg",
parents=[parent_parser],
description=append_doc_link(PKG_HELP, "pkg"),
help=PKG_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
add_help=False,
)
pkg_subparsers = pkg_parser.add_subparsers(
dest="cmd", help="Use dvc pkg CMD --help for command-specific help."
)
fix_subparsers(pkg_subparsers)
PKG_INSTALL_HELP = "Install package."
pkg_install_parser = pkg_subparsers.add_parser(
"install",
parents=[parent_config_parser, parent_parser],
description=append_doc_link(PKG_INSTALL_HELP, "pkg-install"),
help=PKG_INSTALL_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
pkg_install_parser.add_argument(
"address",
nargs="?",
default="",
help="Package address: git://<url> or https://github.com/...",
)
pkg_install_parser.add_argument(
"target_dir",
metavar="target",
nargs="?",
default=".",
help="Target directory to deploy package outputs. "
"Default value is the current dir.",
)
pkg_install_parser.add_argument(
"-s",
"--select",
metavar="OUT",
action="append",
default=[],
help="Select and persist only specified outputs from a package. "
"The parameter can be used multiple times. "
"All outputs will be selected by default.",
)
pkg_install_parser.add_argument(
"-f",
"--file",
help="Specify name of the stage file. It should be "
"either 'Dvcfile' or have a '.dvc' suffix (e.g. "
"'prepare.dvc', 'clean.dvc', etc). "
"By default the file has 'mod_' prefix and imported package name "
"followed by .dvc",
)
pkg_install_parser.set_defaults(func=CmdPkg)
+33 -12
View File
@@ -1,12 +1,13 @@
from __future__ import unicode_literals
import argparse
import os
import re
import dvc.logger as logger
from dvc.command.base import fix_subparsers
from dvc.config import Config
from dvc.command.base import append_doc_link, fix_subparsers
from dvc.command.config import CmdConfig
from dvc.config import Config
class CmdRemoteAdd(CmdConfig):
@@ -38,6 +39,14 @@ class CmdRemoteAdd(CmdConfig):
)
section = Config.SECTION_REMOTE_FMT.format(self.args.name)
if (section in self.configobj.keys()) and not self.args.force:
logger.error(
"Remote with name {} already exists. "
"Use -f (--force) to overwrite remote "
"with new value".format(self.args.name)
)
return 1
ret = self._set(section, Config.SECTION_REMOTE_URL, self.args.url)
if ret != 0:
return ret
@@ -119,12 +128,13 @@ class CmdRemoteList(CmdConfig):
def add_parser(subparsers, parent_parser):
from dvc.command.config import parent_config_parser
REMOTE_HELP = "Manage set of tracked repositories."
REMOTE_HELP = "Manage remote storage configuration."
remote_parser = subparsers.add_parser(
"remote",
parents=[parent_parser],
description=REMOTE_HELP,
description=append_doc_link(REMOTE_HELP, "remote"),
help=REMOTE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_subparsers = remote_parser.add_subparsers(
@@ -138,14 +148,14 @@ def add_parser(subparsers, parent_parser):
remote_add_parser = remote_subparsers.add_parser(
"add",
parents=[parent_config_parser, parent_parser],
description=REMOTE_ADD_HELP,
description=append_doc_link(REMOTE_ADD_HELP, "remote-add"),
help=REMOTE_ADD_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_add_parser.add_argument("name", help="Name.")
remote_add_parser.add_argument(
"url",
help="URL. See full list of supported urls at "
"https://dvc.org/doc/commands-reference/remote",
help="URL. See full list of supported urls at " "man.dvc.org/remote",
)
remote_add_parser.add_argument(
"-d",
@@ -154,14 +164,22 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Set as default remote.",
)
remote_add_parser.add_argument(
"-f",
"--force",
action="store_true",
default=False,
help="Force overwriting existing configs",
)
remote_add_parser.set_defaults(func=CmdRemoteAdd)
REMOTE_DEFAULT_HELP = "Set/unset default remote."
remote_default_parser = remote_subparsers.add_parser(
"default",
parents=[parent_config_parser, parent_parser],
description=REMOTE_DEFAULT_HELP,
description=append_doc_link(REMOTE_DEFAULT_HELP, "remote-default"),
help=REMOTE_DEFAULT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_default_parser.add_argument(
"name", nargs="?", help="Name of the remote."
@@ -179,8 +197,9 @@ def add_parser(subparsers, parent_parser):
remote_remove_parser = remote_subparsers.add_parser(
"remove",
parents=[parent_config_parser, parent_parser],
description=REMOTE_REMOVE_HELP,
description=append_doc_link(REMOTE_REMOVE_HELP, "remote-remove"),
help=REMOTE_REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_remove_parser.add_argument("name", help="Name")
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
@@ -189,8 +208,9 @@ def add_parser(subparsers, parent_parser):
remote_modify_parser = remote_subparsers.add_parser(
"modify",
parents=[parent_config_parser, parent_parser],
description=REMOTE_MODIFY_HELP,
description=append_doc_link(REMOTE_MODIFY_HELP, "remote-modify"),
help=REMOTE_MODIFY_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_modify_parser.add_argument("name", help="Name.")
remote_modify_parser.add_argument("option", help="Option.")
@@ -204,11 +224,12 @@ def add_parser(subparsers, parent_parser):
)
remote_modify_parser.set_defaults(func=CmdRemoteModify)
REMOTE_LIST_HELP = "List remotes."
REMOTE_LIST_HELP = "List available remotes."
remote_list_parser = remote_subparsers.add_parser(
"list",
parents=[parent_config_parser, parent_parser],
description=REMOTE_LIST_HELP,
description=append_doc_link(REMOTE_LIST_HELP, "remote-list"),
help=REMOTE_LIST_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remote_list_parser.set_defaults(func=CmdRemoteList)
+5 -2
View File
@@ -1,9 +1,11 @@
from __future__ import unicode_literals
import argparse
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdRemove(CmdBase):
@@ -42,8 +44,9 @@ def add_parser(subparsers, parent_parser):
remove_parser = subparsers.add_parser(
"remove",
parents=[parent_parser],
description=REMOVE_HELP,
description=append_doc_link(REMOVE_HELP, "remove"),
help=REMOVE_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
remove_parser_group = remove_parser.add_mutually_exclusive_group()
remove_parser_group.add_argument(
+12 -6
View File
@@ -1,9 +1,11 @@
from __future__ import unicode_literals
import argparse
import os
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.command.metrics import show_metrics
from dvc.command.status import CmdDataStatus
from dvc.exceptions import DvcException
@@ -40,7 +42,8 @@ class CmdRepro(CmdBase):
logger.info(CmdDataStatus.UP_TO_DATE_MSG)
if self.args.metrics:
self.repo.metrics.show()
metrics = self.repo.metrics.show()
show_metrics(metrics)
except DvcException:
logger.error()
ret = 1
@@ -51,15 +54,18 @@ class CmdRepro(CmdBase):
def add_parser(subparsers, parent_parser):
REPRO_HELP = "Reproduce DVC file. Default file name - 'Dvcfile'."
REPRO_HELP = "Check for changes and reproduce DVC file and dependencies."
repro_parser = subparsers.add_parser(
"repro",
parents=[parent_parser],
description=REPRO_HELP,
description=append_doc_link(REPRO_HELP, "repro"),
help=REPRO_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
repro_parser.add_argument(
"targets", nargs="*", help="DVC file to reproduce."
"targets",
nargs="*",
help="DVC file to reproduce (default - 'Dvcfile').",
)
repro_parser.add_argument(
"-f",
@@ -80,7 +86,7 @@ def add_parser(subparsers, parent_parser):
"-c",
"--cwd",
default=os.path.curdir,
help="Directory within your repo to reroduce from.",
help="Directory within your repo to reproduce from.",
)
repro_parser.add_argument(
"-m",
+8 -3
View File
@@ -1,9 +1,10 @@
from __future__ import unicode_literals
import argparse
import os
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdRoot(CmdBase):
@@ -16,8 +17,12 @@ class CmdRoot(CmdBase):
def add_parser(subparsers, parent_parser):
ROOT_HELP = "Relative path to repo's directory."
ROOT_HELP = "Relative path to the repository's directory."
root_parser = subparsers.add_parser(
"root", parents=[parent_parser], description=ROOT_HELP, help=ROOT_HELP
"root",
parents=[parent_parser],
description=append_doc_link(ROOT_HELP, "root"),
help=ROOT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
root_parser.set_defaults(func=CmdRoot)
+28 -8
View File
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
from dvc.exceptions import DvcException
@@ -18,12 +18,15 @@ class CmdRun(CmdBase):
self.args.outs_no_cache,
self.args.metrics,
self.args.metrics_no_cache,
self.args.outs_persist,
self.args.outs_persist_no_cache,
self.args.command,
]
): # pragma: no cover
logger.error(
"too few arguments. Specify at least one: '-d', "
"'-o', '-O', '-m', '-M', 'command'."
"too few arguments. Specify at least one: '-d', '-o', '-O',"
" '-m', '-M', '--outs-persist', '--outs-persist-no-cache',"
" 'command'."
)
return 1
@@ -43,6 +46,8 @@ class CmdRun(CmdBase):
ignore_build_cache=self.args.ignore_build_cache,
remove_outs=self.args.remove_outs,
no_commit=self.args.no_commit,
outs_persist=self.args.outs_persist,
outs_persist_no_cache=self.args.outs_persist_no_cache,
)
except DvcException:
logger.error("failed to run command")
@@ -73,12 +78,13 @@ class CmdRun(CmdBase):
def add_parser(subparsers, parent_parser):
RUN_HELP = (
"Generate a stage file from a given "
"command and execute the command."
)
RUN_HELP = "Generate a stage file from a command and execute the command."
run_parser = subparsers.add_parser(
"run", parents=[parent_parser], description=RUN_HELP, help=RUN_HELP
"run",
parents=[parent_parser],
description=append_doc_link(RUN_HELP, "run"),
help=RUN_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
run_parser.add_argument(
"-d",
@@ -174,6 +180,20 @@ def add_parser(subparsers, parent_parser):
default=False,
help="Don't put files/directories into cache.",
)
run_parser.add_argument(
"--outs-persist",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro.",
)
run_parser.add_argument(
"--outs-persist-no-cache",
action="append",
default=[],
help="Declare output file or directory that will not be "
"removed upon repro (do not put into DVC cache).",
)
run_parser.add_argument(
"command", nargs=argparse.REMAINDER, help="Command to execute."
)
+151
View File
@@ -0,0 +1,151 @@
import yaml
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase, fix_subparsers, append_doc_link
class CmdTagAdd(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.repo.tag.add(
self.args.tag,
target=target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.error("failed to add tag")
return 1
return 0
class CmdTagRemove(CmdBase):
def run(self):
for target in self.args.targets:
try:
self.repo.tag.remove(
self.args.tag,
target=target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
except DvcException:
logger.error("failed to remove tag")
return 1
return 0
class CmdTagList(CmdBase):
def run(self):
for target in self.args.targets:
try:
tags = self.repo.tag.list(
target,
with_deps=self.args.with_deps,
recursive=self.args.recursive,
)
if tags:
logger.info(yaml.dump(tags, default_flow_style=False))
except DvcException:
logger.error("failed list tags")
return 1
return 0
def add_parser(subparsers, parent_parser):
TAG_HELP = "A set of commands to manage DVC tags."
tag_parser = subparsers.add_parser(
"tag",
parents=[parent_parser],
description=append_doc_link(TAG_HELP, "tag"),
add_help=False,
)
tag_subparsers = tag_parser.add_subparsers(
dest="cmd",
help="Use DVC tag CMD --help to display command-specific help.",
)
fix_subparsers(tag_subparsers)
TAG_ADD_HELP = "Add DVC tag."
tag_add_parser = tag_subparsers.add_parser(
"add",
parents=[parent_parser],
description=append_doc_link(TAG_ADD_HELP, "tag-add"),
help=TAG_ADD_HELP,
)
tag_add_parser.add_argument("tag", help="Dvc tag.")
tag_add_parser.add_argument(
"targets", nargs="*", default=[None], help="Dvc files."
)
tag_add_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Add tag for all dependencies of the specified DVC file.",
)
tag_add_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Add tag for subdirectories of the specified directory.",
)
tag_add_parser.set_defaults(func=CmdTagAdd)
TAG_REMOVE_HELP = "Remove DVC tag."
tag_remove_parser = tag_subparsers.add_parser(
"remove",
parents=[parent_parser],
description=append_doc_link(TAG_REMOVE_HELP, "tag-remove"),
help=TAG_REMOVE_HELP,
)
tag_remove_parser.add_argument("tag", help="Dvc tag.")
tag_remove_parser.add_argument(
"targets", nargs="*", default=[None], help="Dvc files."
)
tag_remove_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="Remove tag for all dependencies of the specified DVC file.",
)
tag_remove_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="Remove tag for subdirectories of the specified directory.",
)
tag_remove_parser.set_defaults(func=CmdTagRemove)
TAG_LIST_HELP = "List DVC tags."
tag_list_parser = tag_subparsers.add_parser(
"list",
parents=[parent_parser],
description=append_doc_link(TAG_LIST_HELP, "tag-list"),
help=TAG_LIST_HELP,
)
tag_list_parser.add_argument(
"targets", nargs="*", default=[None], help="Dvc files."
)
tag_list_parser.add_argument(
"-d",
"--with-deps",
action="store_true",
default=False,
help="List tags for all dependencies of the specified DVC file.",
)
tag_list_parser.add_argument(
"-R",
"--recursive",
action="store_true",
default=False,
help="List tags for subdirectories of the specified directory.",
)
tag_list_parser.set_defaults(func=CmdTagList)
+6 -3
View File
@@ -1,8 +1,10 @@
from __future__ import unicode_literals
import argparse
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.command.base import CmdBase
from dvc.command.base import CmdBase, append_doc_link
class CmdUnprotect(CmdBase):
@@ -18,12 +20,13 @@ class CmdUnprotect(CmdBase):
def add_parser(subparsers, parent_parser):
UNPROTECT_HELP = "Unprotect data file/directory."
UNPROTECT_HELP = "Unprotect a data file/directory."
unprotect_parser = subparsers.add_parser(
"unprotect",
parents=[parent_parser],
description=UNPROTECT_HELP,
description=append_doc_link(UNPROTECT_HELP, "unprotect"),
help=UNPROTECT_HELP,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
unprotect_parser.add_argument(
"targets", nargs="+", help="Data files/directory."
+1 -1
View File
@@ -83,7 +83,7 @@ def daemon(args):
os.path.dirname(os.path.dirname(file_path))
)
logger.debug("Trying to spawn '{}' with env '{}'".format(cmd, env))
logger.debug("Trying to spawn '{}'".format(cmd))
if os.name == "nt":
_spawn_windows(cmd, env)
+28
View File
@@ -227,3 +227,31 @@ class RecursiveAddingWhileUsingFilename(DvcException):
super(RecursiveAddingWhileUsingFilename, self).__init__(
"using fname with recursive is not allowed."
)
class OverlappingOutputPathsError(DvcException):
def __init__(self, out_1, out_2):
super(OverlappingOutputPathsError, self).__init__(
"Paths for outs:\n'{}'('{}')\n'{}'('{}')\noverlap. To avoid "
"unpredictable behaviour, rerun command with non overlapping outs "
"paths.".format(
str(out_1),
out_1.stage.relpath,
str(out_2),
out_2.stage.relpath,
)
)
class TargetNotDirectoryError(DvcException):
def __init__(self, path):
super(TargetNotDirectoryError, self).__init__(
"Target: {} is not a directory".format(path)
)
class CheckoutErrorSuggestGit(DvcException):
def __init__(self, target, cause):
super(CheckoutErrorSuggestGit, self).__init__(
"Did you mean 'git checkout {}'?".format(target), cause=cause
)
+8 -5
View File
@@ -22,6 +22,10 @@ def info(message):
logger.info(message)
# Do not annotate debug with progress_aware. Why? Debug messages are called
# in analytics.py. Trigger introduced by @progress_aware may run even
# if log_level != DEBUG (look at Progress.print) Which might result in
# messy terminal after any command (because analytics run in separate thread)
def debug(message):
"""Prints a debug message."""
prefix = colorize("Debug", color="blue")
@@ -203,16 +207,16 @@ def colorize(message, color=None):
)
def _init_colorama():
colorama.init()
def set_default_level():
"""Sets default log level."""
logger.setLevel(logging.INFO)
def _add_handlers():
# NOTE: We need to initialize colorama before setting the stream handlers
# so it can wrap stdout/stderr and convert color codes to Windows.
colorama.init()
formatter = "%(message)s"
class _LogLevelFilter(logging.Filter):
@@ -309,4 +313,3 @@ logger = logging.getLogger("dvc") # pylint: disable=invalid-name
set_default_level()
_add_handlers()
_init_colorama()
+71 -19
View File
@@ -33,38 +33,69 @@ OUTS_MAP = {
"local": OutputLOCAL,
}
SCHEMA = {
OutputBase.PARAM_PATH: str,
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH, GS);
# 2) etag (S3);
# 3) checksum (HDFS);
#
# so when a few types of outputs share the same name, we only need
# specify it once.
# NOTE: currently there are only 3 possible checksum names:
#
# 1) md5 (LOCAL, SSH, GS);
# 2) etag (S3);
# 3) checksum (HDFS);
#
# so when a few types of outputs share the same name, we only need
# specify it once.
CHECKSUM_SCHEMA = {
schema.Optional(RemoteLOCAL.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(RemoteS3.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(RemoteHDFS.PARAM_CHECKSUM): schema.Or(str, None),
schema.Optional(OutputBase.PARAM_CACHE): bool,
schema.Optional(OutputBase.PARAM_METRIC): OutputBase.METRIC_SCHEMA,
}
TAGS_SCHEMA = {schema.Optional(str): CHECKSUM_SCHEMA}
def _get(stage, p, info, cache, metric):
SCHEMA = CHECKSUM_SCHEMA.copy()
SCHEMA[OutputBase.PARAM_PATH] = str
SCHEMA[schema.Optional(OutputBase.PARAM_CACHE)] = bool
SCHEMA[schema.Optional(OutputBase.PARAM_METRIC)] = OutputBase.METRIC_SCHEMA
SCHEMA[schema.Optional(OutputBase.PARAM_TAGS)] = TAGS_SCHEMA
SCHEMA[schema.Optional(OutputBase.PARAM_PERSIST)] = bool
def _get(stage, p, info, cache, metric, persist=False, tags=None):
parsed = urlparse(p)
if parsed.scheme == "remote":
name = Config.SECTION_REMOTE_FMT.format(parsed.netloc)
sect = stage.repo.config.config[name]
remote = Remote(stage.repo, sect)
return OUTS_MAP[remote.scheme](
stage, p, info, cache=cache, remote=remote, metric=metric
stage,
p,
info,
cache=cache,
remote=remote,
metric=metric,
persist=persist,
tags=tags,
)
for o in OUTS:
if o.supported(p):
return o(stage, p, info, cache=cache, remote=None, metric=metric)
return OutputLOCAL(stage, p, info, cache=cache, remote=None, metric=metric)
return o(
stage,
p,
info,
cache=cache,
remote=None,
metric=metric,
persist=persist,
tags=tags,
)
return OutputLOCAL(
stage,
p,
info,
cache=cache,
remote=None,
metric=metric,
persist=persist,
tags=tags,
)
def loadd_from(stage, d_list):
@@ -73,12 +104,33 @@ def loadd_from(stage, d_list):
p = d.pop(OutputBase.PARAM_PATH)
cache = d.pop(OutputBase.PARAM_CACHE, True)
metric = d.pop(OutputBase.PARAM_METRIC, False)
ret.append(_get(stage, p, info=d, cache=cache, metric=metric))
persist = d.pop(OutputBase.PARAM_PERSIST, False)
tags = d.pop(OutputBase.PARAM_TAGS, None)
ret.append(
_get(
stage,
p,
info=d,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
)
return ret
def loads_from(stage, s_list, use_cache=True, metric=False):
def loads_from(stage, s_list, use_cache=True, metric=False, persist=False):
ret = []
for s in s_list:
ret.append(_get(stage, s, info={}, cache=use_cache, metric=metric))
ret.append(
_get(
stage,
s,
info={},
cache=use_cache,
metric=metric,
persist=persist,
)
)
return ret
+43 -3
View File
@@ -36,6 +36,7 @@ class OutputBase(object):
PARAM_METRIC = "metric"
PARAM_METRIC_TYPE = "type"
PARAM_METRIC_XPATH = "xpath"
PARAM_PERSIST = "persist"
METRIC_SCHEMA = Or(
None,
@@ -46,11 +47,21 @@ class OutputBase(object):
},
)
PARAM_TAGS = "tags"
DoesNotExistError = OutputDoesNotExistError
IsNotFileOrDirError = OutputIsNotFileOrDirError
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
self.stage = stage
self.repo = stage.repo
@@ -59,6 +70,8 @@ class OutputBase(object):
self.remote = remote or self.REMOTE(self.repo, {})
self.use_cache = False if self.IS_DEPENDENCY else cache
self.metric = False if self.IS_DEPENDENCY else metric
self.persist = persist
self.tags = None if self.IS_DEPENDENCY else (tags or {})
if (
self.use_cache
@@ -84,6 +97,11 @@ class OutputBase(object):
"is local is not supported for {}".format(self.scheme)
)
def assign_to_stage_file(self, target_repo):
raise DvcException(
"change repo is not supported for {}".format(self.scheme)
)
@classmethod
def match(cls, url):
return re.match(cls.REMOTE.REGEX, url)
@@ -181,6 +199,10 @@ class OutputBase(object):
del self.metric[self.PARAM_METRIC_XPATH]
ret[self.PARAM_METRIC] = self.metric
ret[self.PARAM_PERSIST] = self.persist
if self.tags:
ret[self.PARAM_TAGS] = self.tags
return ret
@@ -192,12 +214,20 @@ class OutputBase(object):
def download(self, to_info, resume=False):
self.remote.download([self.path_info], [to_info], resume=resume)
def checkout(self, force=False):
def checkout(self, force=False, progress_callback=None, tag=None):
if not self.use_cache:
return
if tag:
info = self.tags[tag]
else:
info = self.info
getattr(self.repo.cache, self.scheme).checkout(
self.path_info, self.info, force=force
self.path_info,
info,
force=force,
progress_callback=progress_callback,
)
def remove(self, ignore_remove=False):
@@ -220,3 +250,13 @@ class OutputBase(object):
if self.scheme == "local" and self.use_cache and self.is_local:
self.repo.scm.ignore(self.path)
def get_files_number(self):
if self.use_cache and self.checksum:
return 1
return 0
def unprotect(self):
if self.exists:
self.remote.unprotect(self.path_info)
+17 -2
View File
@@ -11,10 +11,25 @@ class OutputHDFS(OutputBase):
REMOTE = RemoteHDFS
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputHDFS, self).__init__(
stage, path, info=info, remote=remote, cache=cache, metric=metric
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
if remote:
path = posixpath.join(remote.url, urlparse(path).path.lstrip("/"))
+50 -2
View File
@@ -14,10 +14,25 @@ class OutputLOCAL(OutputBase):
REMOTE = RemoteLOCAL
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputLOCAL, self).__init__(
stage, path, info, remote=remote, cache=cache, metric=metric
stage,
path,
info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
if remote:
p = os.path.join(
@@ -33,6 +48,8 @@ class OutputLOCAL(OutputBase):
self.path_info = {"scheme": "local", "path": p}
self._dir_cache = {}
def __str__(self):
return self.rel_path
@@ -42,6 +59,17 @@ class OutputLOCAL(OutputBase):
self.url
)
def assign_to_stage_file(self, stage):
from dvc.repo import Repo
fullpath = os.path.abspath(stage.wdir)
self.path_info["path"] = os.path.join(fullpath, self.stage_path)
self.repo = Repo(self.path)
self.stage = stage
return self
@property
def sep(self):
return os.sep
@@ -50,10 +78,18 @@ class OutputLOCAL(OutputBase):
def rel_path(self):
return os.path.relpath(self.path)
@property
def stage_path(self):
return os.path.relpath(self.path, self.stage.wdir)
@property
def cache(self):
return self.repo.cache.local.get(self.checksum)
@property
def is_dir_cache(self):
return self.repo.cache.local.is_dir_cache(self.checksum)
def dumpd(self):
ret = super(OutputLOCAL, self).dumpd()
if self.is_local:
@@ -118,3 +154,15 @@ class OutputLOCAL(OutputBase):
self.repo.scm.ignore(self.path)
self.info = self.remote.save_info(self.path_info)
@property
def dir_cache(self):
return self.repo.cache.local.load_dir_cache(self.checksum)
def get_files_number(self):
if self.cache is None:
return 0
if self.is_dir_cache:
return len(self.dir_cache)
return 1
+17 -2
View File
@@ -11,10 +11,25 @@ class OutputS3(OutputBase):
REMOTE = RemoteS3
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputS3, self).__init__(
stage, path, info=info, remote=remote, cache=cache, metric=metric
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
bucket = remote.bucket if remote else urlparse(path).netloc
path = urlparse(path).path.lstrip("/")
+17 -2
View File
@@ -12,10 +12,25 @@ class OutputSSH(OutputBase):
REMOTE = RemoteSSH
def __init__(
self, stage, path, info=None, remote=None, cache=True, metric=False
self,
stage,
path,
info=None,
remote=None,
cache=True,
metric=False,
persist=False,
tags=None,
):
super(OutputSSH, self).__init__(
stage, path, info=info, remote=remote, cache=cache, metric=metric
stage,
path,
info=info,
remote=remote,
cache=cache,
metric=metric,
persist=persist,
tags=tags,
)
parsed = urlparse(path)
host = remote.host if remote else parsed.hostname
+32 -9
View File
@@ -8,6 +8,8 @@ from dvc.utils.compat import str
import sys
import threading
CLEARLINE_PATTERN = "\r\x1b[K"
class Progress(object):
"""
@@ -30,14 +32,20 @@ class Progress(object):
"""Returns if all targets have finished."""
return self._n_total == self._n_finished
def _clearln(self):
self._print("\r\x1b[K", end="")
def clearln(self):
self.print(CLEARLINE_PATTERN, end="")
def _writeln(self, line):
self._clearln()
self._print(line, end="")
self.clearln()
self.print(line, end="")
sys.stdout.flush()
def reset(self):
with self._lock:
self._n_total = 0
self._n_finished = 0
self._line = None
def refresh(self, line=None):
"""Refreshes progress bar."""
# Just go away if it is locked. Will update next time
@@ -64,9 +72,9 @@ class Progress(object):
pbar = self._bar(name, 100, 100)
if sys.stdout.isatty():
self._clearln()
self.clearln()
self._print(pbar)
self.print(pbar)
self._n_finished += 1
self._line = None
@@ -98,7 +106,7 @@ class Progress(object):
return num + pbar + percent + target_name
@staticmethod
def _print(*args, **kwargs):
def print(*args, **kwargs):
import dvc.logger as logger
if logger.is_quiet():
@@ -109,7 +117,7 @@ class Progress(object):
def __enter__(self):
self._lock.acquire(True)
if self._line is not None:
self._clearln()
self.clearln()
def __exit__(self, typ, value, tbck):
if self._line is not None:
@@ -124,11 +132,26 @@ def progress_aware(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not progress.is_finished:
progress._print()
progress.print()
progress.clearln()
return f(*args, **kwargs)
return wrapper
class ProgressCallback(object):
def __init__(self, total):
self.total = total
self.current = 0
progress.reset()
def update(self, name, progress_to_add=1):
self.current += progress_to_add
progress.update_target(name, self.current, self.total)
def finish(self, name):
progress.finish_target(name)
progress = Progress() # pylint: disable=invalid-name
+7 -1
View File
@@ -6,6 +6,7 @@ import re
try:
from azure.storage.blob import BlockBlobService
from azure.common import AzureMissingResourceHttpError
except ImportError:
BlockBlobService = None
@@ -77,7 +78,12 @@ class RemoteAzure(RemoteBase):
connection_string=self.connection_string
)
logger.debug("Container name {}".format(self.bucket))
self.__blob_service.create_container(self.bucket)
try: # verify that container exists
self.__blob_service.list_blobs(
self.bucket, delimiter="/", num_results=1
)
except AzureMissingResourceHttpError:
self.__blob_service.create_container(self.bucket)
return self.__blob_service
def remove(self, path_info):
+19 -6
View File
@@ -322,7 +322,9 @@ class RemoteBase(object):
self.remove(path_info)
def do_checkout(self, path_info, checksum, force=False):
def do_checkout(
self, path_info, checksum, force=False, progress_callback=None
):
if self.exists(path_info):
msg = "data '{}' exists. Removing before checkout."
logger.warning(msg.format(str(path_info)))
@@ -331,7 +333,9 @@ class RemoteBase(object):
from_info = self.checksum_to_path_info(checksum)
self.copy(from_info, path_info)
def checkout(self, path_info, checksum_info, force=False):
def checkout(
self, path_info, checksum_info, force=False, progress_callback=None
):
scheme = path_info["scheme"]
if scheme not in ["", "local"] and scheme != self.scheme:
raise NotImplementedError
@@ -339,12 +343,12 @@ class RemoteBase(object):
checksum = checksum_info.get(self.PARAM_CHECKSUM)
if not checksum:
msg = "No checksum info for '{}'."
logger.info(msg.format(str(path_info)))
logger.debug(msg.format(str(path_info)))
return
if not self.changed(path_info, checksum_info):
msg = "Data '{}' didn't change."
logger.info(msg.format(str(path_info)))
logger.debug(msg.format(str(path_info)))
return
if self.changed_cache(checksum):
@@ -354,6 +358,15 @@ class RemoteBase(object):
return
msg = "Checking out '{}' with cache '{}'."
logger.info(msg.format(str(path_info), checksum))
logger.debug(msg.format(str(path_info), checksum))
self.do_checkout(path_info, checksum, force=force)
self.do_checkout(
path_info,
checksum,
force=force,
progress_callback=progress_callback,
)
@staticmethod
def unprotect(path_info):
pass
+1 -1
View File
@@ -77,7 +77,7 @@ class RemoteHDFS(RemoteBase):
def rm(self, path_info):
self.hadoop_fs(
"rm {}".format(path_info["path"]), user=path_info["user"]
"rm -f {}".format(path_info["path"]), user=path_info["user"]
)
def save_info(self, path_info):
+74 -19
View File
@@ -20,7 +20,15 @@ from dvc.remote.base import (
STATUS_DELETED,
STATUS_MISSING,
)
from dvc.utils import remove, move, copyfile, dict_md5, to_chunks, tmp_fname
from dvc.utils import (
remove,
move,
copyfile,
dict_md5,
to_chunks,
tmp_fname,
walk_files,
)
from dvc.utils import LARGE_DIR_SIZE
from dvc.config import Config
from dvc.exceptions import DvcException
@@ -70,6 +78,8 @@ class RemoteLOCAL(RemoteBase):
self.path_info = {"scheme": "local"}
self._dir_info = {}
@staticmethod
def compat_config(config):
ret = config.copy()
@@ -248,9 +258,12 @@ class RemoteLOCAL(RemoteBase):
def load_dir_cache(self, md5):
path = self.get(md5)
assert self.is_dir_cache(path)
dir_info = self._dir_info.get(md5)
if dir_info:
return dir_info
try:
with open(path, "r") as fd:
d = json.load(fd)
@@ -267,6 +280,8 @@ class RemoteLOCAL(RemoteBase):
for info in d:
info["relpath"] = self.to_ospath(info["relpath"])
self._dir_info[md5] = d
return d
def dump_dir_cache(self, md5, dir_info):
@@ -291,10 +306,11 @@ class RemoteLOCAL(RemoteBase):
def is_dir_cache(cls, cache):
return cache.endswith(cls.MD5_DIR_SUFFIX)
def do_checkout(self, path_info, checksum, force=False):
def do_checkout(
self, path_info, checksum, force=False, progress_callback=None
):
path = path_info["path"]
md5 = checksum
cache = self.get(md5)
cache = self.get(checksum)
if not self.is_dir_cache(cache):
if os.path.exists(path):
@@ -302,6 +318,8 @@ class RemoteLOCAL(RemoteBase):
self.link(cache, path)
self.state.update_link(path)
if progress_callback:
progress_callback.update(os.path.relpath(path))
return
# Create dir separately so that dir is created
@@ -309,12 +327,11 @@ class RemoteLOCAL(RemoteBase):
if not os.path.exists(path):
os.makedirs(path)
dir_info = self.load_dir_cache(md5)
dir_relpath = os.path.relpath(path)
dir_size = len(dir_info)
bar = dir_size > LARGE_DIR_SIZE
logger.info("Linking directory '{}'.".format(dir_relpath))
logger.debug("Linking directory '{}'.".format(dir_relpath))
dir_info = self.load_dir_cache(checksum)
for processed, entry in enumerate(dir_info):
relpath = entry[self.PARAM_RELPATH]
@@ -332,16 +349,13 @@ class RemoteLOCAL(RemoteBase):
self.link(c, p)
if bar:
progress.update_target(dir_relpath, processed, dir_size)
if progress_callback:
progress_callback.update(os.path.relpath(p))
self._discard_working_directory_changes(path, dir_info, force=force)
self.state.update_link(path)
if bar:
progress.finish_target(dir_relpath)
def already_cached(self, path_info):
assert path_info["scheme"] in ["", "local"]
@@ -353,11 +367,7 @@ class RemoteLOCAL(RemoteBase):
return not self.changed_cache(current_md5)
def _discard_working_directory_changes(self, path, dir_info, force=False):
working_dir_files = set(
os.path.join(root, file)
for root, _, files in os.walk(str(path))
for file in files
)
working_dir_files = set(path for path in walk_files(path))
cached_files = set(
os.path.join(path, file["relpath"]) for file in dir_info
@@ -739,3 +749,48 @@ class RemoteLOCAL(RemoteBase):
"nor on remote. Missing cache files: {}".format(missing_desc)
)
logger.warning(msg)
@staticmethod
def _unprotect_file(path):
if System.is_symlink(path) or System.is_hardlink(path):
logger.debug("Unprotecting '{}'".format(path))
tmp = os.path.join(os.path.dirname(path), "." + str(uuid.uuid4()))
# The operations order is important here - if some application
# would access the file during the process of copyfile then it
# would get only the part of file. So, at first, the file should be
# copied with the temporary name, and then original file should be
# replaced by new.
copyfile(
path,
tmp,
name="Unprotecting '{}'".format(os.path.relpath(path)),
)
remove(path)
os.rename(tmp, path)
else:
logger.debug(
"Skipping copying for '{}', since it is not "
"a symlink or a hardlink.".format(path)
)
os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
@staticmethod
def _unprotect_dir(path):
for path in walk_files(path):
RemoteLOCAL._unprotect_file(path)
@staticmethod
def unprotect(path_info):
path = path_info["path"]
if not os.path.exists(path):
raise DvcException(
"can't unprotect non-existing data '{}'".format(path)
)
if os.path.isdir(path):
RemoteLOCAL._unprotect_dir(path)
else:
RemoteLOCAL._unprotect_file(path)
+17 -2
View File
@@ -1,3 +1,4 @@
import errno
import os
import posixpath
@@ -127,7 +128,17 @@ class SSHConnection:
self._sftp_connect()
for entry in self._sftp.listdir_attr(directory):
try:
dir_entries = self._sftp.listdir_attr(directory)
except IOError as exc:
raise DvcException(
"couldn't get the '{}' remote directory files list".format(
directory
),
cause=exc,
)
for entry in dir_entries:
path = posixpath.join(directory, entry.filename)
if S_ISLNK(entry.st_mode):
@@ -143,7 +154,11 @@ class SSHConnection:
def remove(self, path):
self._sftp_connect()
self._sftp.remove(path)
try:
self._sftp.remove(path)
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
def download(self, src, dest, no_progress_bar=False, progress_title=None):
self._sftp_connect()
+45 -19
View File
@@ -1,12 +1,16 @@
from __future__ import unicode_literals
from dvc.utils.compat import str
import os
import dvc.prompt as prompt
import dvc.logger as logger
from dvc.exceptions import DvcException, NotDvcRepoError, OutputNotFoundError
from dvc.exceptions import (
DvcException,
NotDvcRepoError,
OutputNotFoundError,
TargetNotDirectoryError,
)
class Repo(object):
@@ -28,6 +32,8 @@ class Repo(object):
from dvc.repo.status import status
from dvc.repo.gc import gc
from dvc.repo.commit import commit
from dvc.repo.pkg import install_pkg
from dvc.repo.brancher import brancher
def __init__(self, root_dir=None):
from dvc.config import Config
@@ -38,6 +44,8 @@ class Repo(object):
from dvc.data_cloud import DataCloud
from dvc.updater import Updater
from dvc.repo.metrics import Metrics
from dvc.scm.tree import WorkingTree
from dvc.repo.tag import Tag
root_dir = self.find_root(root_dir)
@@ -46,6 +54,8 @@ class Repo(object):
self.config = Config(self.dvc_dir)
self.tree = WorkingTree()
self.scm = SCM(self.root_dir, repo=self)
self.lock = Lock(self.dvc_dir)
# NOTE: storing state and link_state in the repository itself to avoid
@@ -61,6 +71,7 @@ class Repo(object):
self.updater = Updater(self.dvc_dir)
self.metrics = Metrics(self)
self.tag = Tag(self)
self._ignore()
@@ -97,11 +108,9 @@ class Repo(object):
init(root_dir=root_dir, no_scm=no_scm, force=force)
return Repo(root_dir)
@staticmethod
def unprotect(target):
from dvc.repo.unprotect import unprotect
return unprotect(target)
def unprotect(self, target):
path_info = {"schema": "local", "path": target}
return self.cache.local.unprotect(path_info)
def _ignore(self):
flist = [
@@ -132,25 +141,27 @@ class Repo(object):
raise CyclicGraphError(cycles[0])
def _get_pipeline(self, node):
pipelines = list(filter(lambda g: node in g.nodes(), self.pipelines()))
pipelines = [i for i in self.pipelines() if i.has_node(node)]
assert len(pipelines) == 1
return pipelines[0]
def collect(self, target, with_deps=False):
def collect(self, target, with_deps=False, recursive=False):
import networkx as nx
from dvc.stage import Stage
if not target or recursive:
return self.active_stages(target)
stage = Stage.load(self, target)
if not with_deps:
return [stage]
node = os.path.relpath(stage.path, self.root_dir)
G = self._get_pipeline(node)
stages = nx.get_node_attributes(G, "stage")
ret = [stage]
ret = []
for n in nx.dfs_postorder_nodes(G, node):
ret.append(stages[n])
ret.append(G.node[n]["stage"])
return ret
@@ -243,7 +254,7 @@ class Repo(object):
cache["ssh"] = []
cache["azure"] = []
for branch in self.scm.brancher(
for branch in self.brancher(
all_branches=all_branches, all_tags=all_tags
):
if target:
@@ -282,6 +293,7 @@ class Repo(object):
from dvc.exceptions import (
OutputDuplicationError,
StagePathAsOutputError,
OverlappingOutputPathsError,
)
G = nx.DiGraph()
@@ -292,7 +304,15 @@ class Repo(object):
for stage in stages:
for out in stage.outs:
existing = [o.stage for o in outs if o.path == out.path]
existing = []
for o in outs:
if o.path == out.path:
existing.append(o.stage)
in_o_dir = out.path.startswith(o.path + o.sep)
in_out_dir = o.path.startswith(out.path + out.sep)
if in_o_dir or in_out_dir:
raise OverlappingOutputPathsError(o, out)
if existing:
stages = [stage.relpath, existing[0].relpath]
@@ -357,13 +377,15 @@ class Repo(object):
if not from_directory:
from_directory = self.root_dir
elif not os.path.isdir(from_directory):
raise TargetNotDirectoryError(from_directory)
stages = []
outs = []
for root, dirs, files in os.walk(str(from_directory)):
for root, dirs, files in self.tree.walk(from_directory):
for fname in files:
path = os.path.join(root, fname)
if not Stage.is_stage_file(path):
if not Stage.is_valid_filename(path):
continue
stage = Stage.load(self, path)
for out in stage.outs:
@@ -396,10 +418,14 @@ class Repo(object):
def find_outs_by_path(self, path, outs=None, recursive=False):
if not outs:
outs = [out for stage in self.stages() for out in stage.outs]
# there is no `from_directory=path` argument because some data
# files might be generated to an upper level, and so it is
# needed to look at all the files (project root_dir)
stages = self.stages()
outs = [out for stage in stages for out in stage.outs]
abs_path = os.path.abspath(path)
is_dir = os.path.isdir(abs_path)
is_dir = self.tree.isdir(abs_path)
def func(out):
if out.path == abs_path:
+1 -1
View File
@@ -1,6 +1,6 @@
import os
from dvc.scm import scm_context
from dvc.repo.scm_context import scm_context
from dvc.stage import Stage
from dvc.utils import walk_files
from dvc.exceptions import RecursiveAddingWhileUsingFilename
+56
View File
@@ -0,0 +1,56 @@
def brancher( # noqa: E302
self, branches=None, all_branches=False, tags=None, all_tags=False
):
"""Generator that iterates over specified revisions.
Args:
branches (list): a list of branches to iterate over.
all_branches (bool): iterate over all available branches.
tags (list): a list of tags to iterate over.
all_tags (bool): iterate over all available tags.
Yields:
str: the display name for the currently selected tree, it could be:
- a git revision identifier
- empty string it there is no branches to iterate over
- "Working Tree" if there are uncommited changes in the SCM repo
"""
if not any([branches, all_branches, tags, all_tags]):
yield ""
return
saved_tree = self.tree
revs = []
scm = self.scm
if self.scm.is_dirty():
from dvc.scm.tree import WorkingTree
self.tree = WorkingTree()
yield "Working Tree"
if all_branches:
branches = scm.list_branches()
if all_tags:
tags = scm.list_tags()
if branches is None:
revs.extend([scm.active_branch()])
else:
revs.extend(branches)
if tags is not None:
revs.extend(tags)
# NOTE: it might be a good idea to wrap this loop in try/finally block
# to don't leave the tree on some unexpected branch after the
# `brancher()`, but this could cause problems on exception handling
# code which might expect the tree on which exception was raised to
# stay in place. This behavior is a subject to change.
for rev in revs:
self.tree = scm.get_tree(rev)
yield rev
self.tree = saved_tree
+25 -13
View File
@@ -1,7 +1,8 @@
from __future__ import unicode_literals
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.exceptions import CheckoutErrorSuggestGit
from dvc.progress import ProgressCallback
def _cleanup_unused_links(self, all_stages):
@@ -12,23 +13,32 @@ def _cleanup_unused_links(self, all_stages):
self.state.remove_unused_links(used)
def get_all_files_numbers(stages):
return sum(stage.get_all_files_number() for stage in stages)
def get_progress_callback(stages):
total_files_num = get_all_files_numbers(stages)
if total_files_num == 0:
return None
return ProgressCallback(total_files_num)
def checkout(self, target=None, with_deps=False, force=False, recursive=False):
from dvc.stage import StageFileDoesNotExistError, StageFileBadNameError
if target and not recursive:
all_stages = self.active_stages()
try:
stages = self.collect(target, with_deps=with_deps)
except (StageFileDoesNotExistError, StageFileBadNameError) as exc:
raise DvcException(
str(exc) + " Did you mean 'git checkout {}'?".format(target)
)
else:
all_stages = self.active_stages(target)
stages = all_stages
all_stages = self.stages()
try:
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
except (StageFileDoesNotExistError, StageFileBadNameError) as exc:
if not target:
raise
raise CheckoutErrorSuggestGit(target, exc)
with self.state:
_cleanup_unused_links(self, all_stages)
progress_callback = get_progress_callback(stages)
for stage in stages:
if stage.locked:
@@ -37,4 +47,6 @@ def checkout(self, target=None, with_deps=False, force=False, recursive=False):
" not going to be checked out.".format(path=stage.relpath)
)
stage.checkout(force=force)
stage.checkout(force=force, progress_callback=progress_callback)
if progress_callback:
progress_callback.finish("Checkout finished!")
+1 -5
View File
@@ -1,9 +1,5 @@
def commit(self, target, with_deps=False, recursive=False, force=False):
if target and not recursive:
stages = self.collect(target, with_deps=with_deps)
else:
stages = self.active_stages(target)
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
with self.state:
for stage in stages:
stage.check_can_commit(force=force)
+1 -1
View File
@@ -1,4 +1,4 @@
from dvc.scm import scm_context
from dvc.repo.scm_context import scm_context
@scm_context
+2 -2
View File
@@ -4,7 +4,7 @@ import colorama
import dvc.logger as logger
from dvc.repo import Repo
from dvc.scm import SCM, Base
from dvc.scm import SCM, NoSCM
from dvc.config import Config
from dvc.exceptions import InitError
@@ -56,7 +56,7 @@ def init(root_dir=os.curdir, no_scm=False, force=False):
root_dir = os.path.abspath(root_dir)
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
scm = SCM(root_dir)
if type(scm) == Base and not no_scm:
if isinstance(scm, NoSCM) and not no_scm:
raise InitError(
"{repo} is not tracked by any supported scm tool (e.g. git). "
"Use '--no-scm' if you don't want to use any scm.".format(
+123 -20
View File
@@ -7,7 +7,7 @@ from jsonpath_rw import parse
import dvc.logger as logger
from dvc.exceptions import OutputNotFoundError, BadMetricError, NoMetricsError
from dvc.utils.compat import builtin_str, open
from dvc.utils.compat import builtin_str, open, StringIO, csv_reader
def _read_metric_json(fd, json_path):
@@ -66,33 +66,103 @@ def _read_typed_metric(typ, xpath, fd):
return ret
def _read_metric(path, typ=None, xpath=None, branch=None):
ret = None
if not os.path.exists(path):
return ret
def _format_csv(content, delimiter):
"""Format delimited text to have same column width.
Args:
content (str): The content of a metric.
delimiter (str): Value separator
Returns:
str: Formatted content.
Example:
>>> content = (
"value_mse,deviation_mse,data_set\n"
"0.421601,0.173461,train\n"
"0.67528,0.289545,testing\n"
"0.671502,0.297848,validation\n"
)
>>> _format_csv(content, ",")
"value_mse deviation_mse data_set\n"
"0.421601 0.173461 train\n"
"0.67528 0.289545 testing\n"
"0.671502 0.297848 validation\n"
"""
reader = csv_reader(StringIO(content), delimiter=builtin_str(delimiter))
rows = [row for row in reader]
max_widths = [max(map(len, column)) for column in zip(*rows)]
lines = [
" ".join(
"{entry:{width}}".format(entry=entry, width=width + 2)
for entry, width in zip(row, max_widths)
)
for row in rows
]
return "\n".join(lines)
def _format_output(content, typ):
"""Tabularize the content according to its type.
Args:
content (str): The content of a metric.
typ (str): The type of metric -- (raw|json|tsv|htsv|csv|hcsv).
Returns:
str: Content in a raw or tabular format.
"""
if "csv" in str(typ):
return _format_csv(content, delimiter=",")
if "tsv" in str(typ):
return _format_csv(content, delimiter="\t")
return content
def _read_metric(fd, typ=None, xpath=None, rel_path=None, branch=None):
typ = typ.lower().strip() if typ else typ
xpath = xpath.strip() if xpath else xpath
try:
with open(path, "r") as fd:
if not xpath:
ret = fd.read().strip()
else:
ret = _read_typed_metric(typ, xpath, fd)
if xpath:
return _read_typed_metric(typ, xpath.strip(), fd)
else:
return _format_output(fd.read().strip(), typ)
# Json path library has to be replaced or wrapped in
# order to fix this too broad except clause.
except Exception:
logger.warning(
"unable to read metric in '{}' in branch '{}'".format(
path, branch
rel_path, branch
),
parse_exception=True,
)
return ret
return None
def _collect_metrics(self, path, recursive, typ, xpath, branch):
"""Gather all the metric outputs.
Args:
path (str): Path to a metric file or a directory.
recursive (bool): If path is a directory, do a recursive search for
metrics on the given path.
typ (str): The type of metric to search for, could be one of the
following (raw|json|tsv|htsv|csv|hcsv).
xpath (str): Path to search for.
branch (str): Branch to look up for metrics.
Returns:
list(tuple): (output, typ, xpath)
- output:
- typ:
- xpath:
"""
outs = [out for stage in self.stages() for out in stage.outs]
if path:
@@ -123,16 +193,50 @@ def _collect_metrics(self, path, recursive, typ, xpath, branch):
return res
def _read_metrics_filesystem(path, typ, xpath, rel_path, branch):
if not os.path.exists(path):
return None
with open(path, "r") as fd:
return _read_metric(
fd, typ=typ, xpath=xpath, rel_path=rel_path, branch=branch
)
def _read_metrics(self, metrics, branch):
"""Read the content of each metric file and format it.
Args:
metrics (list): List of metric touples
branch (str): Branch to look up for metrics.
Returns:
A dict mapping keys with metrics path name and content.
For example:
{'metric.csv': ("value_mse deviation_mse data_set\n"
"0.421601 0.173461 train\n"
"0.67528 0.289545 testing\n"
"0.671502 0.297848 validation\n")}
"""
res = {}
for out, typ, xpath in metrics:
assert out.scheme == "local"
if not typ:
typ = os.path.splitext(out.path.lower())[1].replace(".", "")
if out.use_cache:
path = self.cache.local.get(out.checksum)
metric = _read_metrics_filesystem(
self.cache.local.get(out.checksum),
typ=typ,
xpath=xpath,
rel_path=out.rel_path,
branch=branch,
)
else:
path = out.path
fd = self.tree.open(out.path)
metric = _read_metric(
fd, typ=typ, xpath=xpath, rel_path=out.rel_path, branch=branch
)
metric = _read_metric(path, typ=typ, xpath=xpath, branch=branch)
if not metric:
continue
@@ -151,9 +255,8 @@ def show(
recursive=False,
):
res = {}
for branch in self.scm.brancher(
all_branches=all_branches, all_tags=all_tags
):
for branch in self.brancher(all_branches=all_branches, all_tags=all_tags):
entries = _collect_metrics(self, path, recursive, typ, xpath, branch)
metrics = _read_metrics(self, entries, branch)
if metrics:
+1 -1
View File
@@ -1,7 +1,7 @@
import os
from dvc.exceptions import MoveNotDataSourceError
from dvc.scm import scm_context
from dvc.repo.scm_context import scm_context
def _expand_target_path(from_path, to_path):
+201
View File
@@ -0,0 +1,201 @@
from __future__ import unicode_literals
import os
import shutil
from git.cmd import Git
import dvc.logger as logger
from dvc.exceptions import DvcException
from dvc.stage import Stage
from dvc.temp_git_repo import TempGitRepo
class PackageParams(object):
def __init__(self, address, target_dir, select=[], file=None):
self.address = address
self.target_dir = target_dir
self.select = select
self.file = file
@property
def all_addresses(self):
if self.address:
return [self.address]
return PackageManager.read_packages()
class PackageManager(object):
PACKAGE_FILE = "package.yaml"
@staticmethod
def read_packages():
return []
@staticmethod
def get_package(addr):
for pkg_class in [GitPackage]:
try:
return pkg_class()
except Exception:
pass
return None
def __init__(self, addr):
self._addr = addr
class Package(object):
MODULES_DIR = "dvc_mod"
def install_or_update(self, parent_repo, pkg_param):
raise NotImplementedError(
"A method of abstract Package class was called"
)
def is_in_root(self):
return True
class GitPackage(Package):
DEF_DVC_FILE_PREFIX = "mod_"
def install_or_update(self, parent_repo, pkg_params):
if not self.is_in_root():
raise DvcException(
"This command can be run only from a repository root"
)
if not os.path.exists(self.MODULES_DIR):
logger.debug("Creating modules dir {}".format(self.MODULES_DIR))
os.makedirs(self.MODULES_DIR)
parent_repo.scm.ignore(os.path.abspath(self.MODULES_DIR))
module_name = (
Git.polish_url(pkg_params.address).strip("/").split("/")[-1]
)
if not module_name:
raise DvcException(
"Package address error: unable to extract package name"
)
with TempGitRepo(
pkg_params.address, module_name, Package.MODULES_DIR
) as tmp_repo:
outputs_to_copy = tmp_repo.outs
if pkg_params.select:
outputs_to_copy = list(
filter(
lambda out: out.dvc_path in pkg_params.select,
outputs_to_copy,
)
)
fetched_stage_files = set(
map(lambda o: o.stage.path, outputs_to_copy)
)
tmp_repo.fetch(fetched_stage_files)
module_dir = self.create_module_dir(module_name)
tmp_repo.persist_to(module_dir, parent_repo)
dvc_file = self.get_dvc_file_name(
pkg_params.file, pkg_params.target_dir, module_name
)
try:
self.persist_stage_and_scm_state(
parent_repo,
outputs_to_copy,
pkg_params.target_dir,
dvc_file,
)
except Exception as ex:
raise DvcException(
"Package '{}' was installed "
"but stage file '{}' "
"was not created properly: {}".format(
pkg_params.address, dvc_file, ex
)
)
parent_repo.checkout(dvc_file)
@staticmethod
def persist_stage_and_scm_state(
parent_repo, outputs_to_copy, target_dir, dvc_file
):
stage = Stage.create(
repo=parent_repo,
fname=dvc_file,
validate_state=False,
wdir=target_dir,
)
stage.outs = list(
map(lambda o: o.assign_to_stage_file(stage), outputs_to_copy)
)
for out in stage.outs:
parent_repo.scm.ignore(out.path, in_curr_dir=True)
stage.dump()
@staticmethod
def create_module_dir(module_name):
module_dir = os.path.join(GitPackage.MODULES_DIR, module_name)
if os.path.exists(module_dir):
logger.info("Updating package {}".format(module_name))
shutil.rmtree(module_dir)
else:
logger.info("Adding package {}".format(module_name))
return module_dir
def get_dvc_file_name(self, stage_file, target_dir, module_name):
if stage_file:
dvc_file_path = stage_file
else:
dvc_file_name = self.DEF_DVC_FILE_PREFIX + module_name + ".dvc"
dvc_file_path = os.path.join(target_dir, dvc_file_name)
return dvc_file_path
def install_pkg(self, pkg_params):
"""
Install package.
The command can be run only from DVC project root.
E.g.
Having: DVC package in https://github.com/dmpetrov/tag_classifier
$ dvc pkg install https://github.com/dmpetrov/tag_classifier
Result: tag_classifier package in dvc_mod/ directory
"""
if not os.path.isdir(pkg_params.target_dir):
logger.error(
"Unable to install package: "
"target directory '{}' does not exist".format(
pkg_params.target_dir
)
)
return 1
curr_dir = os.path.realpath(os.curdir)
if not os.path.realpath(pkg_params.target_dir).startswith(curr_dir):
logger.error(
"Unable to install package: the current dir should be"
" a subdirectory of the target dir {}".format(
pkg_params.target_dir
)
)
return 1
for addr in pkg_params.all_addresses:
try:
mgr = PackageManager.get_package(addr)
mgr.install_or_update(self, pkg_params)
except Exception as ex:
logger.error("Unable to install package: ".format(ex))
return 1
return 0
+2 -2
View File
@@ -3,8 +3,8 @@ def remove(self, target, outs_only=False):
stage = Stage.load(self, target)
if outs_only:
stage.remove_outs()
stage.remove_outs(force=True)
else:
stage.remove()
stage.remove(force=True)
return stage
+1 -1
View File
@@ -4,7 +4,7 @@ import os
import dvc.logger as logger
from dvc.exceptions import ReproductionError
from dvc.scm import scm_context
from dvc.repo.scm_context import scm_context
def _reproduce_stage(stages, node, force, dry, interactive, no_commit):
+9 -1
View File
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
from dvc.scm import scm_context
from dvc.repo.scm_context import scm_context
@scm_context
@@ -20,6 +20,8 @@ def run(
ignore_build_cache=False,
remove_outs=False,
no_commit=False,
outs_persist=None,
outs_persist_no_cache=None,
):
from dvc.stage import Stage
@@ -33,6 +35,10 @@ def run(
metrics = []
if metrics_no_cache is None:
metrics_no_cache = []
if outs_persist is None:
outs_persist = []
if outs_persist_no_cache is None:
outs_persist_no_cache = []
with self.state:
stage = Stage.create(
@@ -49,6 +55,8 @@ def run(
overwrite=overwrite,
ignore_build_cache=ignore_build_cache,
remove_outs=remove_outs,
outs_persist=outs_persist,
outs_persist_no_cache=outs_persist_no_cache,
)
if stage is None:
+12
View File
@@ -0,0 +1,12 @@
def scm_context(method):
def run(repo, *args, **kw):
try:
result = method(repo, *args, **kw)
repo.scm.reset_ignores()
repo.scm.remind_to_track()
return result
except Exception as e:
repo.scm.cleanup_ignores()
raise e
return run
+2 -5
View File
@@ -6,10 +6,7 @@ import dvc.logger as logger
def _local_status(self, target=None, with_deps=False):
status = {}
if target:
stages = self.collect(target, with_deps=with_deps)
else:
stages = self.active_stages()
stages = self.collect(target, with_deps=with_deps)
for stage in stages:
if stage.locked:
@@ -54,7 +51,7 @@ def _cloud_status(
for md5, info in status_info.items():
name = info["name"]
status = info["status"]
if status == cloud.STATUS_OK:
if status in [cloud.STATUS_OK, cloud.STATUS_MISSING]:
continue
prefix_map = {cloud.STATUS_DELETED: "deleted", cloud.STATUS_NEW: "new"}
+18
View File
@@ -0,0 +1,18 @@
class Tag(object):
def __init__(self, repo):
self.repo = repo
def add(self, *args, **kwargs):
from dvc.repo.tag.add import add
return add(self.repo, *args, **kwargs)
def list(self, *args, **kwargs):
from dvc.repo.tag.list import list
return list(self.repo, *args, **kwargs)
def remove(self, *args, **kwargs):
from dvc.repo.tag.remove import remove
return remove(self.repo, *args, **kwargs)
+15
View File
@@ -0,0 +1,15 @@
import dvc.logger as logger
def add(self, tag, target=None, with_deps=False, recursive=False):
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
for stage in stages:
changed = False
for out in stage.outs:
if not out.info:
logger.warning("missing checksum info for '{}'".format(out))
continue
out.tags[tag] = out.info.copy()
changed = True
if changed:
stage.dump()
+11
View File
@@ -0,0 +1,11 @@
def list(self, target=None, with_deps=False, recursive=False):
ret = {}
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
for stage in stages:
outs_tags = {}
for out in stage.outs:
if out.tags:
outs_tags[str(out)] = out.tags.copy()
if outs_tags:
ret.update({stage.relpath: outs_tags})
return ret
+15
View File
@@ -0,0 +1,15 @@
import dvc.logger as logger
def remove(self, tag, target=None, with_deps=False, recursive=False):
stages = self.collect(target, with_deps=with_deps, recursive=recursive)
for stage in stages:
changed = False
for out in stage.outs:
if tag not in out.tags.keys():
logger.warning("tag '{}' not found for '{}'".format(tag, out))
continue
del out.tags[tag]
changed = True
if changed:
stage.dump()
-48
View File
@@ -1,48 +0,0 @@
from __future__ import unicode_literals
import os
import stat
import uuid
import dvc.logger as logger
from dvc.system import System
from dvc.utils import copyfile, move, remove
from dvc.exceptions import DvcException
def _unprotect_file(path):
if System.is_symlink(path) or System.is_hardlink(path):
logger.debug("Unprotecting '{}'".format(path))
tmp = os.path.join(os.path.dirname(path), "." + str(uuid.uuid4()))
move(path, tmp)
copyfile(tmp, path)
remove(tmp)
else:
logger.debug(
"Skipping copying for '{}', since it is not "
"a symlink or a hardlink.".format(path)
)
os.chmod(path, os.stat(path).st_mode | stat.S_IWRITE)
def _unprotect_dir(path):
for root, dirs, files in os.walk(str(path)):
for f in files:
path = os.path.join(root, f)
_unprotect_file(path)
def unprotect(path):
if not os.path.exists(path):
raise DvcException(
"can't unprotect non-existing data '{}'".format(path)
)
if os.path.isdir(path):
_unprotect_dir(path)
else:
_unprotect_file(path)
+6 -18
View File
@@ -5,7 +5,11 @@ from __future__ import unicode_literals
from dvc.scm.base import Base
from dvc.scm.git import Git
import dvc
# just a sugar to point that this is an actual implementation for a dvc
# project under no SCM control
class NoSCM(Base):
pass
def SCM(root_dir, repo=None): # pylint: disable=invalid-name
@@ -22,20 +26,4 @@ def SCM(root_dir, repo=None): # pylint: disable=invalid-name
if Git.is_repo(root_dir) or Git.is_submodule(root_dir):
return Git(root_dir, repo=repo)
return Base(root_dir, repo=repo)
def scm_context(method):
def run(*args, **kw):
repo = args[0]
assert isinstance(repo, dvc.repo.Repo)
try:
result = method(*args, **kw)
repo.scm.reset_ignores()
repo.scm.remind_to_track()
return result
except Exception as e:
repo.scm.cleanup_ignores()
raise e
return run
return NoSCM(root_dir, repo=repo)
+10 -42
View File
@@ -17,12 +17,17 @@ class FileNotInRepoError(DvcException):
"""
class FileNotInTargetSubdirError(DvcException):
"""Thrown when trying to place .gitignore for a file that not in
the file subdirectory."""
class Base(object):
"""Base class for source control management driver implementations."""
def __init__(self, root_dir=os.curdir, repo=None):
self.repo = repo
self.root_dir = root_dir
self.root_dir = os.path.realpath(root_dir)
def __repr__(self):
return "{class_name}: '{directory}'".format(
@@ -76,47 +81,6 @@ class Base(object):
def tag(self, tag):
"""Makes SCM create a tag with a specified name."""
def brancher(
self, branches=None, all_branches=False, tags=None, all_tags=False
):
"""Generator that iterates over specified revisions.
Args:
branches (list): a list of branches to iterate over.
all_branches (bool): iterate over all available branches.
tags (list): a list of tags to iterate over.
all_tags (bool): iterate over all available tags.
Yields:
str: the current revision.
"""
if not branches and not all_branches and not tags and not all_tags:
yield ""
return
saved = self.active_branch()
revs = []
if all_branches:
branches = self.list_branches()
if all_tags:
tags = self.list_tags()
if branches is None:
revs.extend([saved])
else:
revs.extend(branches)
if tags is not None:
revs.extend(tags)
for rev in revs:
self.checkout(rev)
yield rev
self.checkout(saved)
def untracked_files(self): # pylint: disable=no-self-use
"""Returns a list of untracked files."""
return []
@@ -125,6 +89,10 @@ class Base(object):
"""Returns whether or not a specified path is tracked."""
return False
def is_dirty(self):
"""Return whether the SCM contains uncommited changes."""
return False
def active_branch(self): # pylint: disable=no-self-use
"""Returns current branch in the repo."""
return ""
+59 -27
View File
@@ -2,17 +2,18 @@
from __future__ import unicode_literals
# NOTE: need this one because python 2 would re-import this module
# (called git.py) instead of importing gitpython's git module.
from __future__ import absolute_import
from dvc.utils.compat import str, open
import os
import dvc.logger as logger
from dvc.utils.compat import str, open
from dvc.utils import fix_env
from dvc.scm.base import Base, SCMError, FileNotInRepoError
from dvc.scm.base import (
Base,
SCMError,
FileNotInRepoError,
FileNotInTargetSubdirError,
)
from dvc.scm.git.tree import GitTree
import dvc.logger as logger
class Git(Base):
@@ -62,28 +63,47 @@ class Git(Base):
def ignore_file(self):
return self.GITIGNORE
def _get_gitignore(self, path):
assert os.path.isabs(path)
# NOTE: using '/' prefix to make path unambiguous
entry = "/" + os.path.basename(path)
gitignore = os.path.join(os.path.dirname(path), self.GITIGNORE)
def _get_gitignore(self, path, ignore_file_dir=None):
if not ignore_file_dir:
ignore_file_dir = os.path.dirname(os.path.realpath(path))
if not gitignore.startswith(self.root_dir):
assert os.path.isabs(path)
assert os.path.isabs(ignore_file_dir)
if not path.startswith(ignore_file_dir):
msg = (
"{} file has to be located in one of '{}' subdirectories"
", not outside '{}'"
)
raise FileNotInTargetSubdirError(
msg.format(self.GITIGNORE, path, ignore_file_dir)
)
entry = os.path.relpath(path, ignore_file_dir).replace(os.sep, "/")
# NOTE: using '/' prefix to make path unambiguous
if len(entry) > 0 and entry[0] != "/":
entry = "/" + entry
gitignore = os.path.join(ignore_file_dir, self.GITIGNORE)
if not gitignore.startswith(os.path.realpath(self.root_dir)):
raise FileNotInRepoError(path)
return entry, gitignore
def ignore(self, path):
entry, gitignore = self._get_gitignore(path)
def ignore(self, path, in_curr_dir=False):
base_dir = (
os.path.realpath(os.curdir)
if in_curr_dir
else os.path.dirname(path)
)
entry, gitignore = self._get_gitignore(path, base_dir)
ignore_list = []
if os.path.exists(gitignore):
with open(gitignore, "r") as f:
ignore_list = f.readlines()
filtered = list(
filter(lambda x: x.strip() == entry.strip(), ignore_list)
)
if filtered:
if any(filter(lambda x: x.strip() == entry.strip(), ignore_list)):
return
msg = "Adding '{}' to '{}'.".format(
@@ -91,17 +111,20 @@ class Git(Base):
)
logger.info(msg)
content = entry
if ignore_list:
content = "\n" + content
with open(gitignore, "a") as fobj:
fobj.write(content)
self._add_entry_to_gitignore(entry, gitignore, ignore_list)
self.track_file(os.path.relpath(gitignore))
self.ignored_paths.append(path)
@staticmethod
def _add_entry_to_gitignore(entry, gitignore, ignore_list):
content = entry
if ignore_list:
content = "\n" + content
with open(gitignore, "a", encoding="utf-8") as fobj:
fobj.write(content)
def ignore_remove(self, path):
entry, gitignore = self._get_gitignore(path)
@@ -153,7 +176,13 @@ class Git(Base):
return [os.path.join(self.git.working_dir, fname) for fname in files]
def is_tracked(self, path):
return len(self.git.git.ls_files(path)) != 0
# it is equivalent to `bool(self.git.git.ls_files(path))` by
# functionality, but ls_files fails on unicode filenames
path = os.path.relpath(path, self.root_dir)
return path in [i[0] for i in self.git.index.entries]
def is_dirty(self):
return self.git.is_dirty()
def active_branch(self):
return self.git.active_branch.name
@@ -203,3 +232,6 @@ class Git(Base):
basename = os.path.basename(path)
path_parts = os.path.normpath(path).split(os.path.sep)
return basename == self.ignore_file or Git.GIT_DIR in path_parts
def get_tree(self, rev):
return GitTree(self.git, rev)
+114
View File
@@ -0,0 +1,114 @@
import errno
import os
from dvc.utils.compat import StringIO
from dvc.scm.tree import BaseTree
# see git-fast-import(1)
GIT_MODE_DIR = 0o40000
GIT_MODE_FILE = 0o644
class GitTree(BaseTree):
"""Proxies the repo file access methods to Git objects"""
def __init__(self, git, rev):
"""Create GitTree instance
Args:
git (dvc.scm.Git):
branch:
"""
self.git = git
self.rev = rev
def open(self, path):
relpath = os.path.relpath(path, self.git.working_dir)
obj = self.git_object_by_path(path)
if obj is None:
msg = "No such file in branch '{}'".format(self.rev)
raise IOError(errno.ENOENT, msg, relpath)
if obj.mode == GIT_MODE_DIR:
raise IOError(errno.EISDIR, "Is a directory", relpath)
# GitPython's obj.data_stream is a fragile thing, it is better to
# read it immediately, also it needs to be to decoded if we follow
# the `open()` behavior (since data_stream.read() returns bytes,
# and `open` with default "r" mode returns str)
return StringIO(obj.data_stream.read().decode("utf-8"))
def exists(self, path):
return self.git_object_by_path(path) is not None
def isdir(self, path):
obj = self.git_object_by_path(path)
if obj is None:
return False
return obj.mode == GIT_MODE_DIR
def isfile(self, path):
obj = self.git_object_by_path(path)
if obj is None:
return False
# according to git-fast-import(1) file mode could be 644 or 755
return obj.mode & GIT_MODE_FILE == GIT_MODE_FILE
@staticmethod
def _is_tree_and_contains(obj, path):
if obj.mode != GIT_MODE_DIR:
return False
# see https://github.com/gitpython-developers/GitPython/issues/851
# `return (i in tree)` doesn't work so here is a workaround:
for i in obj:
if i.name == path:
return True
return False
def git_object_by_path(self, path):
path = os.path.relpath(os.path.realpath(path), self.git.working_dir)
assert path.split(os.sep, 1)[0] != ".."
tree = self.git.tree(self.rev)
if not path or path == ".":
return tree
for i in path.split(os.sep):
if not self._is_tree_and_contains(tree, i):
# there is no tree for specified path
return None
tree = tree[i]
return tree
def _walk(self, tree, topdown=True):
dirs, nondirs = [], []
for i in tree:
if i.mode == GIT_MODE_DIR:
dirs.append(i.name)
else:
nondirs.append(i.name)
if topdown:
yield os.path.normpath(tree.path), dirs, nondirs
for i in dirs:
for x in self._walk(tree[i], topdown=True):
yield x
if not topdown:
yield os.path.normpath(tree.path), dirs, nondirs
def walk(self, top, topdown=True):
"""Directory tree generator.
See `os.walk` for the docs. Differences:
- no support for symlinks
- it could raise exceptions, there is no onerror argument
"""
tree = self.git_object_by_path(top)
if tree is None:
raise IOError(errno.ENOENT, "No such file")
for x in self._walk(tree, topdown):
yield x
+65
View File
@@ -0,0 +1,65 @@
import os
from dvc.utils.compat import open
class BaseTree(object):
"""Abstract class to represent access to files"""
def open(self, path):
"""Open file and return a stream."""
def exists(self, path):
"""Test whether a path exists."""
def isdir(self, path):
"""Return true if the pathname refers to an existing directory."""
def isfile(self, path):
"""Test whether a path is a regular file"""
def walk(self, top, topdown=True):
"""Directory tree generator.
See `os.walk` for the docs. Differences:
- no support for symlinks
- it could raise exceptions, there is no onerror argument
"""
class WorkingTree(BaseTree):
"""Proxies the repo file access methods to working tree files"""
def open(self, path):
"""Open file and return a stream."""
return open(path, encoding="utf-8")
def exists(self, path):
"""Test whether a path exists."""
return os.path.exists(path)
def isdir(self, path):
"""Return true if the pathname refers to an existing directory."""
return os.path.isdir(path)
def isfile(self, path):
"""Test whether a path is a regular file"""
return os.path.isfile(path)
def walk(self, top, topdown=True):
"""Directory tree generator.
See `os.walk` for the docs. Differences:
- no support for symlinks
- it could raise exceptions, there is no onerror argument
"""
def onerror(e):
raise e
for root, dirs, files in os.walk(
top, topdown=topdown, onerror=onerror
):
if topdown:
dirs[:] = [i for i in dirs if i not in (".git", ".hg", ".dvc")]
yield os.path.normpath(root), dirs, files
+132 -52
View File
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from dvc.utils.compat import str, open
import re
import os
import yaml
import subprocess
@@ -14,7 +15,7 @@ import dvc.logger as logger
import dvc.dependency as dependency
import dvc.output as output
from dvc.exceptions import DvcException
from dvc.utils import dict_md5, fix_env, load_stage_file
from dvc.utils import dict_md5, fix_env, load_stage_file_fobj
class StageCmdFailedError(DvcException):
@@ -129,6 +130,8 @@ class Stage(object):
Optional(PARAM_LOCKED): bool,
}
TAG_REGEX = r"^(?P<path>.*)@(?P<tag>[^\\/@:]*)$"
def __init__(
self,
repo,
@@ -139,6 +142,7 @@ class Stage(object):
outs=None,
md5=None,
locked=False,
tag=None,
):
if deps is None:
deps = []
@@ -153,6 +157,7 @@ class Stage(object):
self.deps = deps
self.md5 = md5
self.locked = locked
self.tag = tag
def __repr__(self):
return "Stage: '{path}'".format(
@@ -171,7 +176,10 @@ class Stage(object):
@staticmethod
def is_valid_filename(path):
return (
path.endswith(Stage.STAGE_FILE_SUFFIX)
# path.endswith doesn't work for encoded unicode filenames on
# Python 2 and since Stage.STAGE_FILE_SUFFIX is ascii then it is
# not needed to decode the path from py2's str
path[-len(Stage.STAGE_FILE_SUFFIX) :] == Stage.STAGE_FILE_SUFFIX
or os.path.basename(path) == Stage.STAGE_FILE
)
@@ -208,10 +216,12 @@ class Stage(object):
return True
for dep in self.deps:
if dep.changed():
status = dep.status()
if status:
logger.warning(
"Dependency '{dep}' of '{stage}' changed.".format(
dep=dep, stage=self.relpath
"Dependency '{dep}' of '{stage}' changed because it is "
"'{status}'.".format(
dep=dep, stage=self.relpath, status=status[str(dep)]
)
)
return True
@@ -220,10 +230,12 @@ class Stage(object):
def _changed_outs(self):
for out in self.outs:
if out.changed():
status = out.status()
if status:
logger.warning(
"Output '{out}' of '{stage}' changed.".format(
out=out, stage=self.relpath
"Output '{out}' of '{stage}' changed because it is "
"'{status}'".format(
out=out, stage=self.relpath, status=status[str(out)]
)
)
return True
@@ -252,21 +264,25 @@ class Stage(object):
return ret
def remove_outs(self, ignore_remove=False):
"""
Used mainly for `dvc remove --outs`
"""
def remove_outs(self, ignore_remove=False, force=False):
"""Used mainly for `dvc remove --outs` and :func:`Stage.reproduce`."""
for out in self.outs:
out.remove(ignore_remove=ignore_remove)
if out.persist and not force:
out.unprotect()
else:
logger.debug(
"Removing output '{out}' of '{stage}'.".format(
out=out, stage=self.relpath
)
)
out.remove(ignore_remove=ignore_remove)
def unprotect_outs(self):
for out in self.outs:
if out.scheme != "local" or not out.exists:
continue
self.repo.unprotect(out.path)
out.unprotect()
def remove(self):
self.remove_outs(ignore_remove=True)
def remove(self, force=False):
self.remove_outs(ignore_remove=True, force=force)
os.unlink(self.path)
def reproduce(
@@ -275,10 +291,6 @@ class Stage(object):
if not self.changed() and not force:
return None
if (self.cmd or self.is_import) and not self.locked and not dry:
# Removing outputs only if we actually have command to reproduce
self.remove_outs(ignore_remove=False)
msg = (
"Going to reproduce '{stage}'. "
"Are you sure you want to continue?".format(stage=self.relpath)
@@ -396,6 +408,9 @@ class Stage(object):
overwrite=True,
ignore_build_cache=False,
remove_outs=False,
validate_state=True,
outs_persist=None,
outs_persist_no_cache=None,
):
if outs is None:
outs = []
@@ -407,6 +422,10 @@ class Stage(object):
metrics = []
if metrics_no_cache is None:
metrics_no_cache = []
if outs_persist is None:
outs_persist = []
if outs_persist_no_cache is None:
outs_persist_no_cache = []
# Backward compatibility for `cwd` option
if wdir is None and cwd is not None:
@@ -423,13 +442,14 @@ class Stage(object):
stage = Stage(repo=repo, wdir=wdir, cmd=cmd, locked=locked)
stage.outs = output.loads_from(stage, outs, use_cache=True)
stage.outs += output.loads_from(
stage, metrics, use_cache=True, metric=True
)
stage.outs += output.loads_from(stage, outs_no_cache, use_cache=False)
stage.outs += output.loads_from(
stage, metrics_no_cache, use_cache=False, metric=True
Stage._fill_stage_outputs(
stage,
outs,
outs_no_cache,
metrics,
metrics_no_cache,
outs_persist,
outs_persist_no_cache,
)
stage.deps = dependency.loads_from(stage, deps)
@@ -458,23 +478,49 @@ class Stage(object):
else:
stage.unprotect_outs()
if os.path.exists(path):
if not ignore_build_cache and stage.is_cached:
logger.info("Stage is cached, skipping.")
return None
if validate_state:
if os.path.exists(path):
if not ignore_build_cache and stage.is_cached:
logger.info("Stage is cached, skipping.")
return None
msg = (
"'{}' already exists. Do you wish to run the command and "
"overwrite it?".format(stage.relpath)
)
msg = (
"'{}' already exists. Do you wish to run the command and "
"overwrite it?".format(stage.relpath)
)
if not overwrite and not prompt.confirm(msg):
raise StageFileAlreadyExistsError(stage.relpath)
if not overwrite and not prompt.confirm(msg):
raise StageFileAlreadyExistsError(stage.relpath)
os.unlink(path)
os.unlink(path)
return stage
@staticmethod
def _fill_stage_outputs(
stage,
outs,
outs_no_cache,
metrics,
metrics_no_cache,
outs_persist,
outs_persist_no_cache,
):
stage.outs = output.loads_from(stage, outs, use_cache=True)
stage.outs += output.loads_from(
stage, metrics, use_cache=True, metric=True
)
stage.outs += output.loads_from(
stage, outs_persist, use_cache=True, persist=True
)
stage.outs += output.loads_from(stage, outs_no_cache, use_cache=False)
stage.outs += output.loads_from(
stage, metrics_no_cache, use_cache=False, metric=True
)
stage.outs += output.loads_from(
stage, outs_persist_no_cache, use_cache=False, persist=True
)
@staticmethod
def _check_dvc_filename(fname):
if not Stage.is_valid_filename(fname):
@@ -486,19 +532,36 @@ class Stage(object):
)
@staticmethod
def _check_file_exists(fname):
if not os.path.exists(fname):
def _check_file_exists(repo, fname):
if not repo.tree.exists(fname):
raise StageFileDoesNotExistError(fname)
@staticmethod
def load(repo, fname):
Stage._check_file_exists(fname)
Stage._check_dvc_filename(fname)
if not Stage.is_stage_file(fname):
def _check_isfile(repo, fname):
if not repo.tree.isfile(fname):
raise StageFileIsNotDvcFileError(fname)
d = load_stage_file(fname)
@classmethod
def _get_path_tag(cls, s):
regex = re.compile(cls.TAG_REGEX)
match = regex.match(s)
if not match:
return s, None
return match.group("path"), match.group("tag")
@staticmethod
def load(repo, fname):
fname, tag = Stage._get_path_tag(fname)
# it raises the proper exceptions by priority:
# 1. when the file doesn't exists
# 2. filename is not a dvc filename
# 3. path doesn't represent a regular file
Stage._check_file_exists(repo, fname)
Stage._check_dvc_filename(fname)
Stage._check_isfile(repo, fname)
d = load_stage_file_fobj(repo.tree.open(fname), fname)
Stage.validate(d, fname=os.path.relpath(fname))
path = os.path.abspath(fname)
@@ -514,6 +577,7 @@ class Stage(object):
cmd=d.get(Stage.PARAM_CMD),
md5=d.get(Stage.PARAM_MD5),
locked=d.get(Stage.PARAM_LOCKED, False),
tag=tag,
)
stage.deps = dependency.loadd_from(stage, d.get(Stage.PARAM_DEPS, []))
@@ -557,7 +621,7 @@ class Stage(object):
self.repo.scm.track_file(os.path.relpath(fname))
def _compute_md5(self):
from dvc.output.local import OutputLOCAL
from dvc.output.base import OutputBase
d = self.dumpd()
@@ -575,7 +639,15 @@ class Stage(object):
# NOTE: excluding parameters that don't affect the state of the
# pipeline. Not excluding `OutputLOCAL.PARAM_CACHE`, because if
# it has changed, we might not have that output in our cache.
m = dict_md5(d, exclude=[self.PARAM_LOCKED, OutputLOCAL.PARAM_METRIC])
m = dict_md5(
d,
exclude=[
self.PARAM_LOCKED,
OutputBase.PARAM_METRIC,
OutputBase.PARAM_TAGS,
OutputBase.PARAM_PERSIST,
],
)
logger.debug("Computed stage '{}' md5: '{}'".format(self.relpath, m))
return m
@@ -681,6 +753,9 @@ class Stage(object):
raise StageCmdFailedError(self)
def run(self, dry=False, resume=False, no_commit=False, force=False):
if (self.cmd or self.is_import) and not self.locked and not dry:
self.remove_outs(ignore_remove=False, force=False)
if self.locked:
logger.info(
"Verifying outputs in locked stage '{stage}'".format(
@@ -737,9 +812,11 @@ class Stage(object):
if paths:
raise MissingDataSource(paths)
def checkout(self, force=False):
def checkout(self, force=False, progress_callback=None):
for out in self.outs:
out.checkout(force=force)
out.checkout(
force=force, tag=self.tag, progress_callback=progress_callback
)
@staticmethod
def _status(entries):
@@ -782,3 +859,6 @@ class Stage(object):
for out in self.outs
)
)
def get_all_files_number(self):
return sum(out.get_files_number() for out in self.outs)
+16 -1
View File
@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from dvc.utils.compat import str, open
import os
import errno
class System(object):
@@ -61,8 +62,22 @@ class System(object):
@staticmethod
def _reflink_darwin(src, dst):
import ctypes
import dvc.logger as logger
LIBC = "libc.dylib"
LIBC_FALLBACK = "/usr/lib/libSystem.dylib"
try:
clib = ctypes.CDLL(LIBC)
except OSError as exc:
logger.debug(
"unable to access '{}' (errno '{}'). "
"Falling back to '{}'.".format(LIBC, exc.errno, LIBC_FALLBACK)
)
if exc.errno != errno.ENOENT:
raise
# NOTE: trying to bypass System Integrity Protection (SIP)
clib = ctypes.CDLL(LIBC_FALLBACK)
clib = ctypes.CDLL("libc.dylib")
if not hasattr(clib, "clonefile"):
return -1
+145
View File
@@ -0,0 +1,145 @@
import os
import shutil
import tempfile
import git
from dvc import logger
from dvc.exceptions import DvcException
class TempRepoException(DvcException):
"""Raise when temporary package repository has not properly created"""
def __init__(self, temp_repo, msg, cause=None):
m = "temp repository '{}' error: {}".format(temp_repo.addr, msg)
super(TempRepoException, self).__init__(m, cause)
class TempGitRepo(object):
GIT_DIR_TO_REMOVE = ".git"
GIT_FILES_TO_REMOVE = [".gitignore", ".gitmodules"]
SUFFIX = "tmp_DVC_mod"
def __init__(self, addr, module_name, modules_dir):
self.addr = addr
self.modules_dir = modules_dir
self._tmp_mod_prefix = module_name + "_" + self.SUFFIX + "_"
self._reset_state()
def _reset_state(self):
self._set_state(None, [])
def _set_state(self, cloned_tmp_dir, outs):
from dvc.repo import Repo
self.repo = Repo(cloned_tmp_dir) if cloned_tmp_dir else None
self._cloned_tmp_dir = cloned_tmp_dir
self.outs = outs
self._moved_files = []
def fetch(self, targets):
for target in targets:
try:
self.repo.fetch(target)
except Exception as ex:
msg = "error in fetching data from {}: {}".format(
os.path.basename(target), ex
)
raise TempRepoException(self, msg)
@property
def is_state_set(self):
return self._cloned_tmp_dir is not None
def __enter__(self):
if self.is_state_set:
raise TempRepoException(self, "Git repo cloning duplication")
module_temp_dir = None
try:
module_temp_dir = self._clone_to_temp_dir()
self._remove_git_files_and_dirs(module_temp_dir)
outputs = self._read_outputs(module_temp_dir)
self._set_state(module_temp_dir, outputs)
finally:
self._clean_mod_temp_dir(module_temp_dir)
return self
def _clone_to_temp_dir(self):
result = tempfile.mktemp(
prefix=self._tmp_mod_prefix, dir=self.modules_dir
)
logger.debug(
"Cloning git repository {} to temp dir {}".format(
self.addr, result
)
)
git.Repo.clone_from(self.addr, result, depth=1)
return result
def _remove_git_files_and_dirs(self, module_temp_dir):
logger.debug(
"Removing git meta files from {}: {} dif and {}".format(
module_temp_dir,
TempGitRepo.GIT_DIR_TO_REMOVE,
", ".join(TempGitRepo.GIT_FILES_TO_REMOVE),
)
)
shutil.rmtree(
os.path.join(module_temp_dir, TempGitRepo.GIT_DIR_TO_REMOVE)
)
for item in TempGitRepo.GIT_FILES_TO_REMOVE:
fname = os.path.join(module_temp_dir, item)
if os.path.exists(fname):
os.remove(fname)
def _clean_mod_temp_dir(self, module_temp_dir):
if not self.is_state_set:
if module_temp_dir and os.path.exists(module_temp_dir):
shutil.rmtree(module_temp_dir, ignore_errors=True)
def _read_outputs(self, module_temp_dir):
from dvc.repo import Repo
pkg_repo = Repo(module_temp_dir)
stages = pkg_repo.stages()
return [out for s in stages for out in s.outs]
def persist_to(self, module_dir, parent_repo):
if not self.is_state_set:
raise TempRepoException(self, "cannot persist")
tmp_repo_cache = self.repo.cache.local.url
for prefix in os.listdir(tmp_repo_cache):
if len(prefix) != 2:
logger.warning(
"wrong dir format in cache {}: dir {}".format(
tmp_repo_cache, prefix
)
)
self._move_all_cache_files(parent_repo, prefix, tmp_repo_cache)
shutil.move(self._cloned_tmp_dir, module_dir)
self._reset_state()
@staticmethod
def _move_all_cache_files(parent_repo, prefix, tmp_repo_cache):
obj_name = os.path.join(tmp_repo_cache, prefix)
for suffix in os.listdir(obj_name):
src_path = os.path.join(tmp_repo_cache, prefix, suffix)
pre = os.path.join(parent_repo.cache.local.url, prefix)
if not os.path.exists(pre):
os.mkdir(pre)
dest = os.path.join(pre, suffix)
shutil.move(src_path, dest)
def __exit__(self, exc_type, exc_value, traceback):
if self.is_state_set:
shutil.rmtree(self._cloned_tmp_dir)
self._reset_state()
pass
+10 -6
View File
@@ -232,16 +232,20 @@ def current_timestamp():
def load_stage_file(path):
with open(path, "r") as fobj:
return load_stage_file_fobj(fobj, path)
def load_stage_file_fobj(fobj, path):
from dvc.exceptions import StageFileCorruptedError
with open(path, "r") as fobj:
try:
return yaml.safe_load(fobj) or {}
except ScannerError:
raise StageFileCorruptedError(path)
try:
return yaml.safe_load(fobj) or {}
except ScannerError:
raise StageFileCorruptedError(path)
def walk_files(directory):
for root, _, files in os.walk(directory):
for root, _, files in os.walk(str(directory)):
for f in files:
yield os.path.join(root, f)
+35 -5
View File
@@ -27,18 +27,48 @@ def encode(u, encoding=None):
return u.encode(encoding, "replace")
# NOTE: cast_bytes_py2 is taken from
# https://github.com/ipython/ipython_genutils
def csv_reader(unicode_csv_data, dialect=None, **kwargs):
"""csv.reader doesn't support Unicode input, so need to use some tricks
to work around this.
Source: https://docs.python.org/2/library/csv.html#csv-examples
"""
import csv
dialect = dialect or csv.excel
if is_py3:
# Python3 supports encoding by default, so just return the object
for row in csv.reader(unicode_csv_data, dialect=dialect, **kwargs):
yield [cell for cell in row]
else:
# csv.py doesn't do Unicode; encode temporarily as UTF-8:
reader = csv.reader(
utf_8_encoder(unicode_csv_data), dialect=dialect, **kwargs
)
for row in reader:
# decode UTF-8 back to Unicode, cell by cell:
yield [unicode(cell, "utf-8") for cell in row] # noqa: F821
def utf_8_encoder(unicode_csv_data):
"""Source: https://docs.python.org/2/library/csv.html#csv-examples"""
for line in unicode_csv_data:
yield line.encode("utf-8")
def cast_bytes(s, encoding=None):
"""Source: https://github.com/ipython/ipython_genutils"""
if not isinstance(s, bytes):
return encode(s, encoding)
return s
# NOTE _makedirs is taken from
# https://github.com/python/cpython/blob/
# 3ce3dea60646d8a5a1c952469a2eb65f937875b3/Lib/os.py#L196-L226
def _makedirs(name, mode=0o777, exist_ok=False):
"""Source: https://github.com/python/cpython/blob/
3ce3dea60646d8a5a1c952469a2eb65f937875b3/Lib/os.py#L196-L226
"""
head, tail = os.path.split(name)
if not tail:
head, tail = os.path.split(head)
+2 -1
View File
@@ -1,5 +1,5 @@
azure-storage-blob==1.3.0
boto3==1.9.86
boto3==1.9.115
ply>=3.9 # See https://github.com/pyinstaller/pyinstaller/issues/1945
configparser>=3.5.0
zc.lockfile>=1.2.1
@@ -25,3 +25,4 @@ pydot>=1.2.4
asciimatics>=1.10.0
distro>=1.3.0
appdirs>=1.4.3
treelib>=1.5.5
+1 -1
View File
@@ -9,7 +9,7 @@ if [[ $python_ver != "2" ]]; then
exit 0
fi
if [[ $TRAVIS_OS_NAME == "osx" && $TRAVIS_OSX_IMAGE != "xcode10.1" ]]; then
if [[ $TRAVIS_OS_NAME == "osx" && $TRAVIS_OSX_IMAGE != "xcode8.3" ]]; then
echo false
exit 0
fi
+1
View File
@@ -8,6 +8,7 @@ python -mtests
if [[ "$TRAVIS_PULL_REQUEST" == "false" && \
"$TRAVIS_SECURE_ENV_VARS" == "true" && \
"$(python -c 'import sys; print(sys.version_info[0])')" == '2' ]]; then
pip install codecov
codecov
fi
+13 -13
View File
@@ -9,28 +9,28 @@
#----------------------------------------------------------
_dvc_commands='init destroy add import checkout run pull push fetch \
status repro remove move gc config remote metrics install \
root lock unlock pipeline'
status repro remove move unprotect gc config remote metrics \
install root lock unlock pipeline commit'
_dvc_global_options="-h --help -q --quiet -V --verbose"
_dvc_options="-h --help -v --version"
_dvc_init="--no-scm -f --force"
_dvc_destroy="-f --force"
_dvc_add="-R --recursive"
_dvc_run="--no-exec -f --file -c --cwd -d --deps -o --outs -O --outs-no-cache -M --metrics-no-cache -y --yes"
_dvc_pull="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps"
_dvc_push="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps"
_dvc_fetch="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps"
_dvc_status="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps -c --cloud"
_dvc_repro="--dry -f --force -s --single-item -c --cwd -m --metrics -i --interactive -p --pipeline -P --all-pipelines"
_dvc_remove="--dry -o --outs -p --purge"
_dvc_gc="-a --all-branches -T --all-tags -c --cloud -r --remote"
_dvc_config="-u --unset --local"
_dvc_add="-R --recursive -f --file --no-commit"
_dvc_run="--no-exec -f --file -c --cwd -d --deps -o --outs -O --outs-no-cache -M --metrics-no-cache -y --yes --overwrite-dvc-file --ignore-build-cache --remove-outs --no-commit -w --wdir"
_dvc_pull="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps -f --force -R --recursive"
_dvc_push="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps -R --recursive"
_dvc_fetch="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps -R --recursive"
_dvc_status="--show-checksums -j --jobs -r --remote -a --all-branches -T --all-tags -d --with-deps -c --cloud -q --quiet"
_dvc_repro="--dry -f --force -s --single-item -c --cwd -m --metrics -i --interactive -p --pipeline -P --all-pipelines --ignore-build-cache --no-commit -s --single-item"
_dvc_remove="--dry -o --outs -p --purge --force"
_dvc_gc="-a --all-branches -T --all-tags -c --cloud -r --remote -f --force -p --project"
_dvc_config="-u --unset --local --system --global"
_dvc_checkout="$(compgen -G '*.dvc')"
_dvc_move="$(compgen -G '*')"
_dvc_lock="$(compgen -G '*.dvc')"
_dvc_unlock="$(compgen -G '*.dvc')"
_dvc_import=""
_dvc_import="--resume"
_dvc_remote=""
_dvc_metrics=""
_dvc_install=""
+79 -37
View File
@@ -25,6 +25,7 @@ _dvc_commands() {
"repro:Reproduce DVC file. Default file name - 'Dvcfile'."
"remove:Remove outputs of DVC file."
"move:Move output of DVC file."
"unprotect:Unprotect data file/directory."
"gc:Collect garbage."
"config:Get or set config options."
"remote:Manage set of tracked repositories."
@@ -34,6 +35,7 @@ _dvc_commands() {
"lock:Lock DVC file."
"unlock:Unlock DVC file."
"pipeline:Manage pipeline."
"commit:Record changes to the repository."
)
_describe 'dvc commands' _commands
@@ -61,61 +63,80 @@ _dvc_destroy=(
_dvc_add=(
{-R,--recursive}"[Recursively add each file under the directory.]"
{-f,--file}"[Specify name of the stage file.]:File:_files"
"--no-commit[Don't put files/directories into cache.]"
"1:File:_files"
)
_dvc_import=(
"--resume[Resume previously started download.]"
"1:URL:"
"2:Output:"
)
_dvc_checkout=(
"1:DVC files:_files -g '(*.dvc|Dvcfile)'"
{-f,--force}"[Do not prompt when removing working directory files.]"
{-d,--with-deps}"[Checkout all dependencies of the specified target.]"
"1:Stages:_files -g '(*.dvc|Dvcfile)'"
)
_dvc_unprotect=(
"*:Data files:_files"
)
_dvc_run=(
"--no-exec[Only create stage file without actually running it.]"
"--overwrite-dvcfile[Overwrite existing dvc file without asking for confirmation.]"
"--ignore-build-cache[Run this stage even if it has been already ran with the same command/dependencies/outputs/etc before.]"
"--remove-outs[Remove outputs before running the command.]"
"--no-commit[Don't put files/directories into cache.]"
{-f,--file}"[Specify name of the stage file.]:File:_files"
{-c,--cwd}"[Directory to run your command and place state file in.]:CWD:_files -/"
{-c,--cwd}"[Deprecated, use -w and -f instead.]:CWD:_files -/"
{-w,--wdir}"[Directory to run your command and place state file in.]:WDIR:_files -/"
"*"{-d,--deps}"[Declare dependencies for reproducible cmd.]:Dependency:_files"
"*"{-o,--outs}"[Declare output data file or data directory.]:Output data:_files"
"*"{-O,--outs-no-cache}"[Declare output regular file or directory.]:Output regular:_files"
"*"{-M,--metrics-no-cache}"[Declare output metric file or directory]:Metrics (no cache):_files"
"*"{-M,--metrics-no-cache}"[Declare output metric file or directory (do not put into DVC cache)]:Metrics (no cache):_files"
"*"{-m,--metrics}"[Declare output metric file or directory]:Metrics:_files"
{-y,--yes}"[Automatic 'yes' answer to all prompts.]"
)
_dvc_pull=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
"--show-checksums[Show checksums instead of file names]"
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
{-a,--all-branches}"[Fetch cache for all branches]"
{-T,--all-tags}"[Fetch cache for all tags]"
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
{-f,--force}"[Do not prompt when removing working directory files.]"
{-R,--recursive}"[Pull cache for subdirectories of the specified directory.]"
)
_dvc_push=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
"--show-checksums[Show checksums instead of file names]"
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
{-a,--all-branches}"[Fetch cache for all branches]"
{-T,--all-tags}"[Fetch cache for all tags]"
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
{-R,--recursive}"[Push cache for subdirectories of the specified directory.]"
)
_dvc_fetch=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
"--show-checksums[Show checksums instead of file names]"
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
{-a,--all-branches}"[Fetch cache for all branches]"
{-T,--all-tags}"[Fetch cache for all tags]"
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
{-R,--recursive}"[Fetch cache for subdirectories of the specified directory.]"
)
_dvc_status=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
"--show-checksums[Show checksums instead of file names]"
{-j,--jobs}"[Number of jobs to run simultaneously]:Number of jobs:"
{-r,--remote}"[Remote repository to pull from]:Remote repository:"
@@ -123,14 +144,16 @@ _dvc_status=(
{-T,--all-tags}"[Fetch cache for all tags]"
{-d,--with-deps}"[Fetch cache for all dependencies of the specified target]"
{-c,--cloud}"[Show status of a local cache compared to a remote repository]"
{-q,--quiet}"[Suppresses all output. Exit with 0 if pipeline is up]"
)
_dvc_repro=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
"--dry[Only print the commands that would be executed without actually executing]"
"--ignore-build-cache[Reproduce all descendants of a changed stage even if their direct dependencies didn't change.]"
"--no-commit[Don't put files/directories into cache.]"
{-f,--force}"[Reproduce even if dependencies were not changed.]"
{-s,--single-item}"[Reproduce only single data item without recursive dependencies check.]"
{-c,--cwd}"[Directory to reproduce from]:CWD:_files -/"
{-m,--metrics}"[Show metrics after reproduction.]"
{-i,--interactive}"[Ask for confirmation before reproducing each stage.]"
{-p,--pipeline}"[Reproduce the whole pipeline that the specified stage file belongs to.]"
@@ -138,10 +161,11 @@ _dvc_repro=(
)
_dvc_remove=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
"--dry[Only print the commands that would be executed without actually executing]"
{-o,--outs}"[Only remove DVC file outputs.(default)]"
{-p,--purge}"[Remove DVC file and all its outputs]"
{-f,--force}"[Force purge.]"
)
_dvc_move=(
@@ -154,19 +178,28 @@ _dvc_gc=(
{-T,--all-tags}"[Collect garbage for all tags]"
{-c,--cloud}"[Collect garbage in remote repository]"
{-r,--remote}"[Remote repository to collect garbage in]:Remote repository:"
{-f,--force}"[Force garbage collection - automatically agree to all prompts.]"
{-p,--projects}"[Keep data files required by these projects in addition to the current one. Useful if you share a single cache across repos.]:Repos:_files"
)
_dvc_config=(
{-u,--unset}"[Unset option.]"
"--local[Unset option.]"
"--system[Use system config.]"
"--global[Use global config.]"
)
_dvc_remote=(
"1:Task:(add remove modify list)"
"1:Sub command:(add default remove modify list)"
)
_dvc_cache=(
"1:Sub command:(dir)"
)
_dvc_metrics=(
"1:Task:(show add modify remove)"
"1:Sub command:(show add modify remove)"
)
_dvc_install=()
@@ -174,15 +207,22 @@ _dvc_install=()
_dvc_root=()
_dvc_lock=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
)
_dvc_unlock=(
"*:DVC files:_files -g '(*.dvc|Dvcfile)'"
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
)
_dvc_pipeline=(
"1:Task:(show)"
"1:Sub command:(show,list)"
)
_dvc_commit=(
"*:Stages:_files -g '(*.dvc|Dvcfile)'"
{-d,--with-deps}"[Commit all dependencies of the specified target.]"
{-f,--force}"[Commit even if checksums for dependencies/outputs changed.]"
{-R,--recursive}"[Commit cache for subdirectories of the specified directory.]"
)
typeset -A opt_args
@@ -194,26 +234,28 @@ _arguments \
'*::args:->args'
case $words[1] in
init) _arguments $_dvc_global_options $_dvc_init ;;
destroy) _arguments $_dvc_global_options $_dvc_destroy ;;
add) _arguments $_dvc_global_options $_dvc_add ;;
import) _arguments $_dvc_global_options $_dvc_import ;;
checkout) _arguments $_dvc_global_options $_dvc_checkout ;;
run) _arguments $_dvc_global_options $_dvc_run ;;
pull) _arguments $_dvc_global_options $_dvc_pull ;;
push) _arguments $_dvc_global_options $_dvc_push ;;
fetch) _arguments $_dvc_global_options $_dvc_fetch ;;
status) _arguments $_dvc_global_options $_dvc_status ;;
repro) _arguments $_dvc_global_options $_dvc_repro ;;
remove) _arguments $_dvc_global_options $_dvc_remove ;;
move) _arguments $_dvc_global_options $_dvc_move ;;
gc) _arguments $_dvc_global_options $_dvc_gc ;;
config) _arguments $_dvc_global_options $_dvc_config ;;
remote) _arguments $_dvc_global_options $_dvc_remote ;;
metrics) _arguments $_dvc_global_options $_dvc_metrics ;;
install) _arguments $_dvc_global_options $_dvc_install ;;
root) _arguments $_dvc_global_options $_dvc_root ;;
lock) _arguments $_dvc_global_options $_dvc_lock ;;
unlock) _arguments $_dvc_global_options $_dvc_unlock ;;
pipeline) _arguments $_dvc_global_options $_dvc_pipeline ;;
init) _arguments $_dvc_global_options $_dvc_init ;;
destroy) _arguments $_dvc_global_options $_dvc_destroy ;;
add) _arguments $_dvc_global_options $_dvc_add ;;
import) _arguments $_dvc_global_options $_dvc_import ;;
checkout) _arguments $_dvc_global_options $_dvc_checkout ;;
run) _arguments $_dvc_global_options $_dvc_run ;;
pull) _arguments $_dvc_global_options $_dvc_pull ;;
push) _arguments $_dvc_global_options $_dvc_push ;;
fetch) _arguments $_dvc_global_options $_dvc_fetch ;;
status) _arguments $_dvc_global_options $_dvc_status ;;
repro) _arguments $_dvc_global_options $_dvc_repro ;;
remove) _arguments $_dvc_global_options $_dvc_remove ;;
move) _arguments $_dvc_global_options $_dvc_move ;;
gc) _arguments $_dvc_global_options $_dvc_gc ;;
config) _arguments $_dvc_global_options $_dvc_config ;;
remote) _arguments $_dvc_global_options $_dvc_remote ;;
metrics) _arguments $_dvc_global_options $_dvc_metrics ;;
install) _arguments $_dvc_global_options $_dvc_install ;;
root) _arguments $_dvc_global_options $_dvc_root ;;
lock) _arguments $_dvc_global_options $_dvc_lock ;;
unlock) _arguments $_dvc_global_options $_dvc_unlock ;;
pipeline) _arguments $_dvc_global_options $_dvc_pipeline ;;
commit) _arguments $_dvc_global_options $_dvc_commit ;;
unprotect) _arguments $_dvc_global_options $_dvc_unprotect ;;
esac
-3
View File
@@ -1,3 +0,0 @@
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata("checksumdir")
@@ -1,3 +1,9 @@
from PyInstaller.utils.hooks import copy_metadata
datas = copy_metadata("google-cloud-storage")
# NOTE: including "requests" metadata because of google-resumable-media
# bug. See [1] for more info.
#
# [1] https://github.com/googleapis/google-resumable-media-python/issues/59
datas += copy_metadata("requests")
+2 -1
View File
@@ -22,11 +22,12 @@ install_requires = [
"asciimatics>=1.10.0",
"distro>=1.3.0",
"appdirs>=1.4.3",
"treelib>=1.5.5",
]
# Extra dependencies for remote integrations
gs = ["google-cloud-storage==1.13.0"]
s3 = ["boto3==1.7.4"]
s3 = ["boto3==1.9.115"]
azure = ["azure-storage-blob==1.3.0"]
ssh = ["paramiko>=2.4.1"]
all_remotes = gs + s3 + azure + ssh
+4 -6
View File
@@ -13,14 +13,12 @@ os.putenv("DVC_HOME", REPO_ROOT)
os.putenv("DVC_TEST", "true")
if len(sys.argv) == 1:
scope = "--all-modules"
params = ""
else:
scope = " ".join(sys.argv[1:])
params = " ".join(sys.argv[1:])
cmd = (
"nosetests -v --processes=4 --process-timeout=600 --cover-inclusive "
"--cover-erase --cover-package=dvc --with-coverage --with-flaky "
"--logging-clear-handlers "
"{scope} ".format(scope=scope)
"py.test -v -n=4 --timeout=600 --timeout_method=thread --cov=dvc "
"{params} ".format(params=params)
)
check_call(cmd, shell=True)
+131 -11
View File
@@ -1,4 +1,9 @@
# encoding: utf-8
from __future__ import unicode_literals
import os
import shutil
import uuid
import tempfile
@@ -7,10 +12,12 @@ from git.exc import GitCommandNotFound
from unittest import TestCase
import dvc.logger as logger
from dvc.command.remote import CmdRemoteAdd
from dvc.repo import Repo as DvcRepo
from dvc.utils.compat import open, str
class TestDir(TestCase):
class TestDirFixture(object):
GCP_CREDS_FILE = os.path.abspath(
os.path.join("scripts", "ci", "gcp-creds.json")
)
@@ -38,6 +45,15 @@ class TestDir(TestCase):
"import sys\nimport shutil\n"
"shutil.copyfile(sys.argv[1], sys.argv[2])"
)
UNICODE = "тест"
UNICODE_CONTENTS = "проверка"
def __init__(self, root_dir=None):
if root_dir:
os.mkdir(root_dir)
else:
root_dir = self.mkdtemp()
self._root_dir = os.path.realpath(root_dir)
def _pushd(self, d):
self._saved_dir = os.path.realpath(os.curdir)
@@ -52,8 +68,12 @@ class TestDir(TestCase):
if len(dname) > 0 and not os.path.isdir(dname):
os.makedirs(dname)
with open(name, "a") as f:
f.write(contents)
with open(name, "a", encoding="utf-8") as f:
f.write(
contents
if isinstance(contents, str)
else contents.decode("utf-8")
)
@staticmethod
def mkdtemp():
@@ -62,8 +82,6 @@ class TestDir(TestCase):
return tempfile.mkdtemp(prefix=prefix, suffix=suffix)
def setUp(self):
self._root_dir = TestDir.mkdtemp()
self._pushd(self._root_dir)
self.create(self.FOO, self.FOO_CONTENTS)
self.create(self.BAR, self.BAR_CONTENTS)
@@ -72,16 +90,17 @@ class TestDir(TestCase):
os.mkdir(self.DATA_SUB_DIR)
self.create(self.DATA, self.DATA_CONTENTS)
self.create(self.DATA_SUB, self.DATA_SUB_CONTENTS)
self.create(self.UNICODE, self.UNICODE_CONTENTS)
def tearDown(self):
self._popd()
class TestGit(TestDir):
class TestGitFixture(TestDirFixture):
N_RETRIES = 5
def setUp(self):
super(TestGit, self).setUp()
super(TestGitFixture, self).setUp()
# NOTE: handles EAGAIN error on BSD systems (osx in our case).
# Otherwise when running tests you might get this exception:
#
@@ -100,17 +119,118 @@ class TestGit(TestDir):
self.git.index.commit("add code")
class TestGitSubmodule(TestGit):
class TestGitSubmoduleFixture(TestGitFixture):
def __init__(self, root_dir=None):
super(TestGitSubmoduleFixture, self).__init__(root_dir)
def setUp(self):
super(TestGitSubmodule, self).setUp()
super(TestGitSubmoduleFixture, self).setUp()
subrepo = Repo.init()
subrepo_path = "subrepo"
self.git.create_submodule(subrepo_path, subrepo_path, subrepo.git_dir)
self._pushd(subrepo_path)
class TestDvc(TestGit):
class TestDvcFixture(TestGitFixture):
def __init__(self, root_dir=None):
super(TestDvcFixture, self).__init__(root_dir)
def setUp(self):
super(TestDvc, self).setUp()
super(TestDvcFixture, self).setUp()
self.dvc = DvcRepo.init(self._root_dir)
self.dvc.scm.commit("init dvc")
logger.be_verbose()
class TestDvcGitInitializedFixture(TestDvcFixture):
def __init__(self, root_dir=None):
super(TestDvcGitInitializedFixture, self).__init__(root_dir)
def setUp(self):
super(TestDvcGitInitializedFixture, self).setUp()
self.git.init()
class TestDvcDataFileFixture(TestDvcGitInitializedFixture):
DATA_DVC_FILE = "data.dvc"
DATA_DIR_DVC_FILE = "data_sub_dir.dvc"
REMOTE = "myremote2"
def __init__(self, root_dir=None, cache_dir=None):
super(TestDvcDataFileFixture, self).__init__(root_dir)
self.cache_dir = cache_dir
def setUp(self):
super(TestDvcDataFileFixture, self).setUp()
self.dvc.add(self.DATA)
self.dvc.add(self.DATA_SUB_DIR)
if self.cache_dir:
shutil.copytree(self.dvc.cache.local.cache_dir, self.cache_dir)
class MockConfig:
system = None
glob = None
local = None
default = True
name = self.REMOTE
url = self.cache_dir
cmd = CmdRemoteAdd(MockConfig())
cmd.run()
self.git.index.add(
[
os.path.join(self.DATA_DIR, self.DATA_DVC_FILE),
os.path.join(self.DATA_DIR, self.DATA_DIR_DVC_FILE),
os.path.join(self.DATA_DIR, ".gitignore"),
".dvc/config",
self.FOO,
self.BAR,
]
)
self.git.index.commit("Hello world commit")
# NOTE: Inheritance order in the classes below is important.
class TestDir(TestDirFixture, TestCase):
def __init__(self, methodName, root_dir=None):
TestDirFixture.__init__(self, root_dir)
TestCase.__init__(self, methodName)
class TestGit(TestGitFixture, TestCase):
def __init__(self, methodName, root_dir=None):
TestGitFixture.__init__(self, root_dir)
TestCase.__init__(self, methodName)
class TestGitSubmodule(TestGitSubmoduleFixture, TestCase):
def __init__(self, methodName, root_dir=None):
TestGitSubmoduleFixture.__init__(self, root_dir)
TestCase.__init__(self, methodName)
class TestDvc(TestDvcFixture, TestCase):
def __init__(self, methodName, root_dir=None):
TestDvcFixture.__init__(self, root_dir)
TestCase.__init__(self, methodName)
class TestDvcPkg(TestDvcFixture, TestCase):
GIT_PKG = "git_pkg"
CACHE_DIR = "mycache"
def __init__(self, methodName, root_dir=None):
TestDvcFixture.__init__(self, root_dir)
TestCase.__init__(self, methodName)
self.pkg_dir = os.path.join(self._root_dir, self.GIT_PKG)
cache_dir = os.path.join(self._root_dir, self.CACHE_DIR)
self.pkg_fixture = TestDvcDataFileFixture(
root_dir=self.pkg_dir, cache_dir=cache_dir
)
self.pkg_fixture.setUp()
+5 -5
View File
@@ -1,11 +1,11 @@
pytest>=3.3.1
nose>=1.3.7
pytest>=4.4.0
pytest-timeout>=1.3.3
pytest-cov>=2.6.1
pytest-xdist>=1.26.1
flaky>=3.5.3
mock>=2.0.0
coverage>=4.5.1
codecov>=2.0.15
xmltodict>=0.11.0
awscli>=1.14.18
awscli>=1.16.125
google-compute-engine
pywin32; sys_platform == 'win32'
Pygments # required by collective.checkdocs
+2 -1
View File
@@ -39,6 +39,7 @@ class TestAdd(TestDvc):
self.assertEqual(len(stage.deps), 0)
self.assertEqual(stage.cmd, None)
self.assertEqual(stage.outs[0].info["md5"], md5)
self.assertEqual(stage.md5, "ee343f2482f53efffc109be83cc976ac")
def test_unicode(self):
fname = "\xe1"
@@ -475,7 +476,7 @@ class TestShouldCleanUpAfterFailedAdd(TestDvc):
self.assertFalse(os.path.exists(bar_stage_file))
gitignore_content = get_gitignore_content()
self.assertFalse(any(self.BAR in line for line in gitignore_content))
self.assertNotIn("/" + self.BAR, gitignore_content)
class TestShouldNotTrackGitInternalFiles(TestDvc):
+8 -2
View File
@@ -27,18 +27,24 @@ class TestAnalytics(TestDir):
self.assertNotEqual(a.info[a.PARAM_SYSTEM_INFO], {})
@mock.patch.object(os, "getenv", new=_clean_getenv)
@mock.patch(
"dvc.analytics.Analytics._is_enabled_config", return_value=True
)
@mock.patch("requests.post")
def test_send(self, mockpost):
def test_send(self, mockpost, _):
ret = main(["daemon", "analytics", Analytics().dump(), "-v"])
self.assertEqual(ret, 0)
self.assertTrue(mockpost.called)
@mock.patch.object(os, "getenv", new=_clean_getenv)
@mock.patch(
"dvc.analytics.Analytics._is_enabled_config", return_value=True
)
@mock.patch.object(
requests, "post", side_effect=requests.exceptions.RequestException()
)
def test_send_failed(self, mockpost):
def test_send_failed(self, mockpost, _):
ret = main(["daemon", "analytics", Analytics().dump(), "-v"])
self.assertEqual(ret, 0)
+4
View File
@@ -128,6 +128,10 @@ class TestCacheLinkType(TestDvc):
class TestCmdCacheDir(TestDvc):
def test(self):
ret = main(["cache", "dir"])
self.assertEqual(ret, 254)
def test_abs_path(self):
dname = os.path.join(os.path.dirname(self._root_dir), "dir")
ret = main(["cache", "dir", dname])
+148 -28
View File
@@ -1,20 +1,31 @@
import os
import mock
import sys
import re
import yaml
import shutil
import filecmp
import collections
from dvc.logger import logger
from dvc.main import main
from dvc import progress
from dvc.repo import Repo as DvcRepo
from dvc.system import System
from dvc.utils import walk_files
from tests.basic_env import TestDvc
from tests.test_repro import TestRepro
from dvc.stage import Stage
from dvc.stage import Stage, StageFileBadNameError, StageFileDoesNotExistError
from dvc.remote.local import RemoteLOCAL
from dvc.exceptions import DvcException, ConfirmRemoveError
from dvc.exceptions import (
DvcException,
ConfirmRemoveError,
TargetNotDirectoryError,
CheckoutErrorSuggestGit,
)
from mock import patch
from tests.utils.logger import MockLoggerHandlers, ConsoleFontColorsRemover
class TestCheckout(TestRepro):
@@ -121,10 +132,7 @@ class CheckoutBase(TestDvc):
FileInfo = collections.namedtuple("FileInfo", "path inode")
paths = [
os.path.join(root, file)
for output in stage.outs
for root, _, files in os.walk(output.path)
for file in files
path for output in stage.outs for path in walk_files(output.path)
]
return [
@@ -355,8 +363,8 @@ class TestCheckoutDirectory(TestRepro):
class TestCheckoutHook(TestDvc):
@mock.patch("sys.stdout.isatty", return_value=True)
@mock.patch("dvc.utils.compat.input", side_effect=EOFError)
@patch("sys.stdout.isatty", return_value=True)
@patch("dvc.prompt.input", side_effect=EOFError)
def test(self, mock_input, mock_isatty):
""" Test that dvc checkout handles EOFError gracefully, which is what
it will experience when running in a git hook.
@@ -378,27 +386,139 @@ class TestCheckoutSuggestGit(TestRepro):
try:
self.dvc.checkout(target="gitbranch")
except DvcException as exc:
msg = str(exc)
self.assertEqual(
msg,
(
"'gitbranch' does not exist. "
"Did you mean 'git checkout gitbranch'?"
),
)
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.cause, StageFileDoesNotExistError)
self.assertIsNone(exc.cause.cause)
try:
self.dvc.checkout(target=self.FOO)
except DvcException as exc:
msg = str(exc)
self.assertEqual(
msg,
(
"bad stage filename '{}'."
" Stage files should be named 'Dvcfile'"
" or have a '.dvc' suffix (e.g. '{}.dvc')."
" Did you mean 'git checkout {}'?".format(
self.FOO, self.FOO, self.FOO
self.assertIsInstance(exc, CheckoutErrorSuggestGit)
self.assertIsInstance(exc.cause, StageFileBadNameError)
self.assertIsNone(exc.cause.cause)
class TestCheckoutShouldHaveSelfClearingProgressBar(TestDvc):
def setUp(self):
super(TestCheckoutShouldHaveSelfClearingProgressBar, self).setUp()
self._prepare_repo()
def test(self):
self._checkout_and_intercept_std_output()
stdout_calls = self.stdout_mock.method_calls
write_calls = self.filter_out_non_write_calls(stdout_calls)
write_calls = self.filter_out_empty_write_calls(write_calls)
self.write_args = [w_c[1][0] for w_c in write_calls]
progress_bars = self.get_progress_bars()
update_bars = progress_bars[:-1]
finish_bar = progress_bars[-1]
self.assertEqual(4, len(update_bars))
self.assertRegexpMatches(progress_bars[0], ".*\\[#{7} {23}\\] 25%.*")
self.assertRegexpMatches(progress_bars[1], ".*\\[#{15} {15}\\] 50%.*")
self.assertRegexpMatches(progress_bars[2], ".*\\[#{22} {8}\\] 75%.*")
self.assertRegexpMatches(progress_bars[3], ".*\\[#{30}\\] 100%.*")
self.assertCaretReturnFollowsEach(update_bars)
self.assertNewLineFollows(finish_bar)
self.assertAnyEndsWith(update_bars, self.FOO)
self.assertAnyEndsWith(update_bars, self.BAR)
self.assertAnyEndsWith(update_bars, self.DATA)
self.assertAnyEndsWith(update_bars, self.DATA_SUB)
self.assertTrue(finish_bar.endswith("Checkout finished!"))
def filter_out_empty_write_calls(self, calls):
def is_not_empty_write(call):
assert call[0] == "write"
return call[1][0] != ""
return list(filter(is_not_empty_write, calls))
def filter_out_non_write_calls(self, calls):
def is_write_call(call):
return call[0] == "write"
return list(filter(is_write_call, calls))
def _prepare_repo(self):
storage = self.mkdtemp()
ret = main(["remote", "add", "-d", "myremote", storage])
self.assertEqual(0, ret)
ret = main(["add", self.DATA_DIR])
self.assertEqual(0, ret)
ret = main(["add", self.FOO])
self.assertEqual(0, ret)
ret = main(["add", self.BAR])
self.assertEqual(0, ret)
ret = main(["push"])
self.assertEqual(0, ret)
shutil.rmtree(self.DATA_DIR)
os.unlink(self.FOO)
os.unlink(self.BAR)
def _checkout_and_intercept_std_output(self):
with MockLoggerHandlers(
logger
), ConsoleFontColorsRemover(), patch.object(
sys, "stdout"
) as stdout_mock:
self.stdout_mock = logger.handlers[0].stream = stdout_mock
ret = main(["checkout"])
self.assertEqual(0, ret)
def get_progress_bars(self):
pattern = re.compile(".*\\[.{30}\\].*%.*")
return [arg for arg in self.write_args if pattern.match(arg)]
def assertCaretReturnFollowsEach(self, update_bars):
for update_bar in update_bars:
self.assertIn(update_bar, self.write_args)
for index, arg in enumerate(self.write_args):
if arg == update_bar:
self.assertEqual(
progress.CLEARLINE_PATTERN, self.write_args[index + 1]
)
),
)
def assertNewLineFollows(self, finish_bar):
self.assertIn(finish_bar, self.write_args)
for index, arg in enumerate(self.write_args):
if arg == finish_bar:
self.assertEqual("\n", self.write_args[index + 1])
def assertAnyEndsWith(self, update_bars, name):
self.assertTrue(any(ub for ub in update_bars if ub.endswith(name)))
class TestCheckoutTargetRecursiveShouldNotRemoveOtherUsedFiles(TestDvc):
def test(self):
ret = main(["add", self.DATA_DIR, self.FOO, self.BAR])
self.assertEqual(0, ret)
ret = main(["checkout", "-R", self.DATA_DIR])
self.assertEqual(0, ret)
self.assertTrue(os.path.exists(self.FOO))
self.assertTrue(os.path.exists(self.BAR))
class TestCheckoutRecursiveNotDirectory(TestDvc):
def test(self):
ret = main(["add", self.FOO])
self.assertEqual(0, ret)
with self.assertRaises(TargetNotDirectoryError):
self.dvc.checkout(target=self.FOO, recursive=True)
+8 -6
View File
@@ -628,8 +628,7 @@ class TestWarnOnOutdatedStage(TestDvc):
url = get_local_url()
self.main(["remote", "add", "-d", TEST_REMOTE, url])
self.dvc.add(self.FOO)
stage = self.dvc.run(deps=["foo"], outs=["bar"], cmd="echo bar > bar")
stage = self.dvc.run(outs=["bar"], cmd="echo bar > bar")
self.main(["push"])
stage_file_path = stage.relpath
@@ -675,7 +674,7 @@ class TestRecursiveSyncOperations(TestDataCloudBase):
)
self.dvc.add(self.DATA)
self.dvc.add(self.DATA_SUB_CONTENTS)
self.dvc.add(self.DATA_SUB)
def _remove_local_data_files(self):
os.remove(self.DATA)
@@ -799,15 +798,18 @@ class TestShouldWarnOnNoChecksumInLocalAndRemoteCache(TestDvc):
with ConsoleFontColorsRemover(), MockLoggerHandlers(logger):
reset_logger_error_output()
main(["push"])
ret = main(["push"])
self.assertEqual(ret, 0)
self.stderr_contains_message()
reset_logger_error_output()
main(["pull"])
ret = main(["pull", "-f"])
self.assertEqual(ret, 0)
self.stderr_contains_message()
reset_logger_error_output()
main(["status", "-c"])
ret = main(["status", "-c"])
self.assertEqual(ret, 0)
self.stderr_contains_message()
+22 -1
View File
@@ -1,3 +1,5 @@
import dvc
from dvc.utils.compat import str
import os
@@ -9,7 +11,7 @@ from dvc.exceptions import DvcException
from dvc.main import main
from mock import patch, mock_open, call
from tests.basic_env import TestDvc
from tests.utils import reset_logger_error_output
from tests.utils import reset_logger_error_output, spy
from tests.utils.httpd import StaticFileServer
from tests.utils.logger import MockLoggerHandlers, ConsoleFontColorsRemover
@@ -137,3 +139,22 @@ class TestShouldNotResumeDownload(TestInterruptedDownload):
m_handle = m()
expected_calls = [call(b"f"), call(b"o"), call(b"o")]
m_handle.write.assert_has_calls(expected_calls, any_order=False)
class TestShouldRemoveOutsBeforeImport(TestDvc):
def setUp(self):
super(TestShouldRemoveOutsBeforeImport, self).setUp()
tmp_dir = self.mkdtemp()
self.external_source = os.path.join(tmp_dir, "file")
with open(self.external_source, "w") as fobj:
fobj.write("content")
def test(self):
remove_outs_call_counter = spy(dvc.stage.Stage.remove_outs)
with patch.object(
dvc.stage.Stage, "remove_outs", remove_outs_call_counter
):
ret = main(["import", self.external_source])
self.assertEqual(0, ret)
self.assertEqual(1, remove_outs_call_counter.mock.call_count)
+7
View File
@@ -1,10 +1,15 @@
import os
import sys
import unittest
from dvc.main import main
from tests.basic_env import TestDvc
@unittest.skipIf(
sys.platform == "win32", "Git hooks aren't supported on Windows"
)
class TestInstall(TestDvc):
def test(self):
ret = main(["install"])
@@ -20,6 +25,8 @@ class TestInstall(TestDvc):
self.assertTrue(os.path.isfile(hook("pre-commit")))
self.dvc.add(self.FOO)
self.dvc.scm.add([".gitignore", self.FOO + ".dvc"])
self.dvc.scm.commit("add")
os.unlink(self.FOO)
self.dvc.scm.checkout("branch", create_new=True)
+178 -3
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
import os
import json
@@ -109,7 +110,7 @@ class TestMetrics(TestDvc):
)
self.assertEqual(len(ret), 3)
for b in ["foo", "bar", "baz"]:
self.assertSequenceEqual(ret[b]["metric_hcsv"], "branch\n" + b)
self.assertEqual(ret[b]["metric_hcsv"].split(), ["branch", b])
def test_type_case_normalized(self):
ret = self.dvc.metrics.show(
@@ -167,6 +168,119 @@ class TestMetrics(TestDvc):
for b in ["foo", "bar", "baz"]:
self.assertSequenceEqual(ret[b]["metric_hcsv"], [[b]])
def test_formatted_output(self):
with open("metrics.csv", "w") as fobj:
# Labels are in Spanish to test unicode characters
fobj.write(
"valor_mse,desviación_mse,data_set\n"
"0.421601,0.173461,entrenamiento\n"
"0.67528,0.289545,pruebas\n"
"0.671502,0.297848,validación\n"
)
with open("metrics.tsv", "w") as fobj:
# Contains quoted newlines to test output correctness
fobj.write(
"value_mse\tdeviation_mse\tdata_set\n"
"0.421601\t0.173461\ttrain\n"
'0.67528\t0.289545\t"test\\ning"\n'
"0.671502\t0.297848\tvalidation\n"
)
with open("metrics.json", "w") as fobj:
fobj.write(
"{\n"
' "data_set": [\n'
' "train",\n'
' "testing",\n'
' "validation"\n'
" ],\n"
' "deviation_mse": [\n'
' "0.173461",\n'
' "0.289545",\n'
' "0.297848"\n'
" ],\n"
' "value_mse": [\n'
' "0.421601",\n'
' "0.67528",\n'
' "0.671502"\n'
" ]\n"
"}"
)
with open("metrics.txt", "w") as fobj:
fobj.write("ROC_AUC: 0.64\nKS: 78.9999999996\nF_SCORE: 77\n")
self.dvc.run(
fname="testing_metrics_output.dvc",
metrics_no_cache=[
"metrics.csv",
"metrics.tsv",
"metrics.json",
"metrics.txt",
],
)
self.dvc.metrics.modify("metrics.csv", typ="csv")
self.dvc.metrics.modify("metrics.tsv", typ="tsv")
self.dvc.metrics.modify("metrics.json", typ="json")
with MockLoggerHandlers(logger.logger):
reset_logger_standard_output()
ret = main(["metrics", "show"])
self.assertEqual(ret, 0)
expected_csv = (
u"\tmetrics.csv:\n"
u"\t\tvalor_mse desviación_mse data_set \n"
u"\t\t0.421601 0.173461 entrenamiento \n"
u"\t\t0.67528 0.289545 pruebas \n"
u"\t\t0.671502 0.297848 validación"
)
expected_tsv = (
"\tmetrics.tsv:\n"
"\t\tvalue_mse deviation_mse data_set \n"
"\t\t0.421601 0.173461 train \n"
"\t\t0.67528 0.289545 test\\ning \n"
"\t\t0.671502 0.297848 validation"
)
expected_txt = (
"\tmetrics.txt:\n"
"\t\tROC_AUC: 0.64\n"
"\t\tKS: 78.9999999996\n"
"\t\tF_SCORE: 77\n"
)
expected_json = (
"\tmetrics.json:\n"
"\t\t{\n"
'\t\t "data_set": [\n'
'\t\t "train",\n'
'\t\t "testing",\n'
'\t\t "validation"\n'
"\t\t ],\n"
'\t\t "deviation_mse": [\n'
'\t\t "0.173461",\n'
'\t\t "0.289545",\n'
'\t\t "0.297848"\n'
"\t\t ],\n"
'\t\t "value_mse": [\n'
'\t\t "0.421601",\n'
'\t\t "0.67528",\n'
'\t\t "0.671502"\n'
"\t\t ]\n"
"\t\t}"
)
stdout = logger.logger.handlers[0].stream.getvalue()
self.assertIn(expected_tsv, stdout)
self.assertIn(expected_csv, stdout)
self.assertIn(expected_txt, stdout)
self.assertIn(expected_json, stdout)
class TestMetricsRecursive(TestDvc):
def setUp(self):
@@ -206,7 +320,9 @@ class TestMetricsRecursive(TestDvc):
self.assertEqual(ret, 0)
self.dvc.scm.add(["nested"])
self.dvc.scm.add(
["nested", "metric_nested.dvc", "metric_subnested.dvc"]
)
self.dvc.scm.commit("nested metrics")
self.dvc.scm.checkout("master")
@@ -489,7 +605,7 @@ class TestCachedMetrics(TestDvc):
self.assertIsNotNone(stage)
self.assertEqual(stage.relpath, "metrics.json.dvc")
self.dvc.scm.add([".gitignore", "metrics.json.dvc"])
self.dvc.scm.add(["code.py", ".gitignore", "metrics.json.dvc"])
self.dvc.scm.commit(branch)
def _test_metrics(self, func):
@@ -502,6 +618,10 @@ class TestCachedMetrics(TestDvc):
func("one")
func("two")
# TestDvc currently is based on TestGit, so it is safe to use
# scm.git for now
self.dvc.scm.git.git.clean("-fd")
self.dvc = DvcRepo(".")
res = self.dvc.metrics.show(
@@ -535,3 +655,58 @@ class TestCachedMetrics(TestDvc):
def test_run(self):
self._test_metrics(self._do_run)
class TestMetricsType(TestDvc):
branches = ["foo", "bar", "baz"]
files = [
"metric",
"metric.txt",
"metric.json",
"metric.tsv",
"metric.htsv",
"metric.csv",
"metric.hcsv",
]
xpaths = [None, None, "branch", "0,0", "0,branch", "0,0", "0,branch"]
def setUp(self):
super(TestMetricsType, self).setUp()
self.dvc.scm.commit("init")
for branch in self.branches:
self.dvc.scm.checkout(branch, create_new=True)
with open("metric", "w+") as fd:
fd.write(branch)
with open("metric.txt", "w+") as fd:
fd.write(branch)
with open("metric.json", "w+") as fd:
json.dump({"branch": branch}, fd)
with open("metric.csv", "w+") as fd:
fd.write(branch)
with open("metric.hcsv", "w+") as fd:
fd.write("branch\n")
fd.write(branch)
with open("metric.tsv", "w+") as fd:
fd.write(branch)
with open("metric.htsv", "w+") as fd:
fd.write("branch\n")
fd.write(branch)
self.dvc.run(metrics_no_cache=self.files, overwrite=True)
self.dvc.scm.add(self.files + ["metric.dvc"])
self.dvc.scm.commit("metric")
self.dvc.scm.checkout("master")
def test_show(self):
for file_name, xpath in zip(self.files, self.xpaths):
self._do_show(file_name, xpath)
def _do_show(self, file_name, xpath):
ret = self.dvc.metrics.show(file_name, xpath=xpath, all_branches=True)
self.assertEqual(len(ret), 3)
for branch in self.branches:
if isinstance(ret[branch][file_name], list):
self.assertSequenceEqual(ret[branch][file_name], [branch])
else:
self.assertSequenceEqual(ret[branch][file_name], branch)
+9 -6
View File
@@ -173,13 +173,16 @@ class TestMoveFileBetweenDirectories(TestDvc):
class TestMoveFileInsideDirectory(TestDvc):
def test(self):
ret = main(["add", "data_dir/data"])
ret = main(["add", self.DATA])
self.assertEqual(ret, 0)
with cd("data_dir"):
ret = main(["move", "data", "data.txt"])
with cd(self.DATA_DIR):
ret = main(["move", os.path.basename(self.DATA), "data.txt"])
self.assertEqual(ret, 0)
self.assertFalse(os.path.exists("data_dir/data"))
self.assertTrue(os.path.exists("data_dir/data.txt"))
self.assertTrue(os.path.exists("data_dir/data.txt.dvc"))
self.assertFalse(os.path.exists(self.DATA))
data_fullpath = os.path.join(self.DATA_DIR, "data.txt")
dvc_fullpath = os.path.join(self.DATA_DIR, "data.txt.dvc")
self.assertTrue(os.path.exists(data_fullpath))
self.assertTrue(os.path.exists(dvc_fullpath))
+1 -1
View File
@@ -23,7 +23,7 @@ class TestOutScheme(TestDvc):
}
def _get(self, path):
return _get(Stage(self.dvc), path, None, None, None)
return _get(Stage(self.dvc), path, None, None, None, None)
def test(self):
for path, cls in self.TESTS.items():
+4
View File
@@ -34,6 +34,10 @@ class TestPipelineShowSingle(TestDvc):
self.assertEqual(ret, 0)
self.assertTrue(os.path.isfile(self.dotFile))
def test_tree(self):
ret = main(["pipeline", "show", "--tree", self.stage])
self.assertEqual(ret, 0)
def test_ascii_commands(self):
ret = main(["pipeline", "show", "--ascii", self.stage, "--commands"])
self.assertEqual(ret, 0)
+37
View File
@@ -0,0 +1,37 @@
import os
from dvc.repo.pkg import Package, PackageParams
from tests.basic_env import TestDvcPkg
class TestPkgBasic(TestDvcPkg):
NEW_PKG_DIR = "new_pkg"
def setUp(self):
super(TestPkgBasic, self).setUp()
self.to_dir = os.path.join(self._root_dir, self.NEW_PKG_DIR)
os.mkdir(self.to_dir)
self.dvc.install_pkg(
PackageParams(self.pkg_fixture._root_dir, self.to_dir)
)
def test_installed_dirs_and_files(self):
self.assertTrue(os.path.isdir(self.pkg_dir))
mod_dir_fullpath = os.path.join(self._root_dir, Package.MODULES_DIR)
self.assertTrue(os.path.isdir(mod_dir_fullpath))
installed_pkg_dir = os.path.join(mod_dir_fullpath, self.GIT_PKG)
for file in [self.FOO, self.BAR, self.CODE]:
installed_file = os.path.join(installed_pkg_dir, file)
self.assertTrue(os.path.isfile(installed_file))
def test_target_dir_checkout(self):
pkg_data_file = os.path.join(self.to_dir, self.DATA)
self.assertTrue(os.path.exists(pkg_data_file))
self.assertTrue(os.path.isfile(pkg_data_file))
pkg_data_dir = os.path.join(self.to_dir, self.DATA_SUB_DIR)
self.assertTrue(os.path.exists(pkg_data_dir))
self.assertTrue(os.path.isdir(pkg_data_dir))

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