mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-04 07:18:27 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef82cedd00 | ||
|
|
91f3036cf1 | ||
|
|
e4dafb8552 | ||
|
|
625b20833e | ||
|
|
332d2198f4 | ||
|
|
33abc407a1 | ||
|
|
424ee7f1b7 | ||
|
|
1c4a8cf2ca | ||
|
|
76228a2118 | ||
|
|
bbb1c59df2 | ||
|
|
58f65345ff | ||
|
|
66def035bf | ||
|
|
c1b04b88fa | ||
|
|
4c9245a3b3 | ||
|
|
c4b2f682f6 | ||
|
|
1c9588e8d5 | ||
|
|
048e34a6a1 | ||
|
|
59409be7ab | ||
|
|
71c4787553 | ||
|
|
b8a42939ad | ||
|
|
375998c664 | ||
|
|
39989ad233 | ||
|
|
fe1d7e9f1d | ||
|
|
0899b277c0 | ||
|
|
253a0eae13 | ||
|
|
8964967af0 | ||
|
|
520e01f113 | ||
|
|
30212f886b | ||
|
|
8dd264c025 | ||
|
|
ec88fa8dec | ||
|
|
fb99f2e332 | ||
|
|
bb2cf74328 | ||
|
|
bd9eb8faba | ||
|
|
fa16b56cba | ||
|
|
07da9f1e5c | ||
|
|
5d2ac9fca2 | ||
|
|
27af048d3e | ||
|
|
5d85692719 | ||
|
|
6e66ddfcb9 | ||
|
|
00c135f7d5 | ||
|
|
8cbefaaa88 | ||
|
|
0275a0d8d8 | ||
|
|
b51ee18fc0 | ||
|
|
c6a024f83b | ||
|
|
1c0f5cdf4b | ||
|
|
a7e825a038 | ||
|
|
7b97d7249d | ||
|
|
83a2afc7b0 | ||
|
|
0a83027536 | ||
|
|
d809723bc9 | ||
|
|
35e0dd9206 | ||
|
|
047352027f | ||
|
|
81a4cea71a | ||
|
|
ad306d7bf3 | ||
|
|
d3457fb8f9 | ||
|
|
59a143eb08 | ||
|
|
64c1321c9d | ||
|
|
a6ff46ce81 | ||
|
|
b81ace54df | ||
|
|
8495b0521b | ||
|
|
9fa5fb0baf | ||
|
|
efcf4e3312 | ||
|
|
b3d72e11fe | ||
|
|
7d45a55072 | ||
|
|
4181c0ec4a | ||
|
|
5cb775308d | ||
|
|
2b7731e4f9 | ||
|
|
8f190a90ee | ||
|
|
57e476acc4 | ||
|
|
061513a2ae | ||
|
|
9f6f599101 | ||
|
|
74930c01ec | ||
|
|
a2f1367a9a | ||
|
|
5d97f92bdd | ||
|
|
02b147b6f4 | ||
|
|
fa0f182028 | ||
|
|
15c787f2ac | ||
|
|
4af3919062 | ||
|
|
3a469f2e93 | ||
|
|
51a8c782ae | ||
|
|
b579d06c8e | ||
|
|
039cd49259 | ||
|
|
f516dd252a | ||
|
|
706dd05e8d | ||
|
|
5d957b1830 | ||
|
|
387867fbbe | ||
|
|
7c6351d039 | ||
|
|
613ca0bdb5 | ||
|
|
0fb9870de6 | ||
|
|
73e60a8c13 | ||
|
|
9d1dde4051 | ||
|
|
38dc0f85d4 | ||
|
|
d2dbe9649c | ||
|
|
ece6f2d3ee | ||
|
|
b672622324 | ||
|
|
c9391df313 | ||
|
|
a0c229cf9b | ||
|
|
a59f90fac9 | ||
|
|
bba502314d | ||
|
|
bc4bf66d17 | ||
|
|
f7348af1e0 | ||
|
|
4d140a9944 | ||
|
|
2ba73daedc | ||
|
|
7a31a35cbf | ||
|
|
24a14563cc | ||
|
|
a1d3440f85 | ||
|
|
45dcb7c27d | ||
|
|
9ff6fef53a | ||
|
|
9e7dc2640e | ||
|
|
584c97a58d | ||
|
|
b94a18cfb9 | ||
|
|
9ead641c95 | ||
|
|
652f5ab439 | ||
|
|
28a123e727 | ||
|
|
566472f088 | ||
|
|
2e12a31c5a | ||
|
|
d7788887f0 | ||
|
|
87276faef2 | ||
|
|
4042d5232d | ||
|
|
fda14c82d3 | ||
|
|
64be262d18 | ||
|
|
1d6c80001d | ||
|
|
91a7194e4f | ||
|
|
afe539aaba | ||
|
|
f259bc52cd | ||
|
|
db69def834 | ||
|
|
5efc3bbee2 | ||
|
|
7230d87137 | ||
|
|
56833387ce | ||
|
|
593e67318b | ||
|
|
afc183f4e7 | ||
|
|
d201fad1e3 | ||
|
|
5be12b5aac | ||
|
|
c1e5b10cbd | ||
|
|
b47b2c5673 | ||
|
|
939f78948a | ||
|
|
215ea4944c | ||
|
|
4e12e76e19 | ||
|
|
33092e8614 | ||
|
|
ae7b604531 | ||
|
|
b7c174641e | ||
|
|
cb43f9cdda | ||
|
|
791ce22759 | ||
|
|
c70adebc75 | ||
|
|
2c9cb87679 | ||
|
|
b57077af11 | ||
|
|
803bb3eb61 | ||
|
|
99b58d3d05 | ||
|
|
5bbaa28cae | ||
|
|
74bd8137ba | ||
|
|
72a576cefe | ||
|
|
036ce9398e | ||
|
|
13012f9c0c | ||
|
|
a65c7989c8 | ||
|
|
3590fe8414 | ||
|
|
05100122e1 | ||
|
|
9a55b909f8 | ||
|
|
ff54ac1bfb | ||
|
|
57e920aa39 | ||
|
|
ac4f351daf | ||
|
|
2c0116ccf9 | ||
|
|
c03ce4aa0f | ||
|
|
72dcbc0a9e | ||
|
|
fd65f44b7f | ||
|
|
7504b68c12 | ||
|
|
86f3457dec | ||
|
|
219662597b | ||
|
|
536966f9d1 | ||
|
|
076eadceba | ||
|
|
d1e08b6157 | ||
|
|
edfa1741db | ||
|
|
9ee62ef8f0 | ||
|
|
0bb49d2549 | ||
|
|
516d82e411 | ||
|
|
ec312d8601 | ||
|
|
f504245f35 | ||
|
|
bb50fb8664 | ||
|
|
777a9f80ae | ||
|
|
02a52d038a | ||
|
|
7d0a06248d | ||
|
|
2d8c6043ca | ||
|
|
3f996048e8 | ||
|
|
8d9435e8ee | ||
|
|
889a17e2fc | ||
|
|
63972c3d12 | ||
|
|
7027c631c5 | ||
|
|
e8f6cd7f5b | ||
|
|
10f2bb93fa | ||
|
|
2fecc66cb9 | ||
|
|
b47998463f | ||
|
|
d53b740f35 | ||
|
|
9198fa7459 | ||
|
|
6954efc6e6 | ||
|
|
d77af8196e | ||
|
|
c5920b00ab | ||
|
|
ce8036e535 | ||
|
|
b7ad2dc841 | ||
|
|
5ab948f8b6 | ||
|
|
f9fbe97509 | ||
|
|
2fba2e030a | ||
|
|
3e951162b8 | ||
|
|
d74358eecd | ||
|
|
58c9ff0de2 | ||
|
|
f0e991790e | ||
|
|
b7cd785eba | ||
|
|
21a3efe2c7 | ||
|
|
865d355554 | ||
|
|
84712c4acc | ||
|
|
0f16e15453 | ||
|
|
61f30ed5ba | ||
|
|
cde15e3d3e | ||
|
|
7672e324d2 | ||
|
|
130b76c7a5 | ||
|
|
040458a726 | ||
|
|
75e795c491 | ||
|
|
4dac2092c9 | ||
|
|
19eaced304 | ||
|
|
184028f176 | ||
|
|
5f63d538d2 | ||
|
|
324ff4be59 | ||
|
|
3f846a1794 | ||
|
|
b52c925d5f | ||
|
|
4a4f07968c | ||
|
|
09136f2378 | ||
|
|
aaca1401ea | ||
|
|
2aaf065ea3 | ||
|
|
d4ec871a76 | ||
|
|
f44ddcf64e | ||
|
|
b88b3b81c6 | ||
|
|
81d04f887a | ||
|
|
afbb3a0add | ||
|
|
b8aa8ad951 | ||
|
|
9897f7c470 | ||
|
|
cf7f125c14 | ||
|
|
cec4b4e003 | ||
|
|
ff4fcc937b | ||
|
|
1676a7a1cd | ||
|
|
974cf097dd | ||
|
|
597ab98d0e | ||
|
|
833e82a7ea | ||
|
|
3916d4fa07 | ||
|
|
d81b56978f | ||
|
|
1e7efffe42 | ||
|
|
cc16b28a26 | ||
|
|
451c7aac04 | ||
|
|
7b4466c982 | ||
|
|
7ca3a1119c | ||
|
|
2c026066db | ||
|
|
7f17c6aa03 | ||
|
|
9f015fac1d | ||
|
|
666e203bbb | ||
|
|
ce946d1160 | ||
|
|
76b31ac282 | ||
|
|
6093a1fe97 | ||
|
|
d62a54c7b0 | ||
|
|
c922da4f3f | ||
|
|
2a16b021ad | ||
|
|
fc42ca721c | ||
|
|
785dcfeef0 | ||
|
|
9714b14456 | ||
|
|
6a406abde8 | ||
|
|
7ca055e868 | ||
|
|
6a64f86178 | ||
|
|
227dbf44d9 | ||
|
|
2bb7257a06 | ||
|
|
e3831066de | ||
|
|
aa99cc9934 | ||
|
|
26e203242f | ||
|
|
d00bd4dfe5 | ||
|
|
795f001929 | ||
|
|
93d7b9173b | ||
|
|
bc7f177c7a | ||
|
|
6f73a67c60 | ||
|
|
68a0c39ad1 | ||
|
|
c186a8ea8f | ||
|
|
af8ac006ad | ||
|
|
b2ea279c9e | ||
|
|
2dda540db6 | ||
|
|
76e6ba24f7 | ||
|
|
505b88afae | ||
|
|
415a85c6be | ||
|
|
4e9b38970a | ||
|
|
3e1eb6d1e7 | ||
|
|
c873787d73 | ||
|
|
e553511085 | ||
|
|
827c994e74 | ||
|
|
22c60dd608 | ||
|
|
f9088e1c67 | ||
|
|
71d156ac49 | ||
|
|
d998c0a536 | ||
|
|
af12f0d06c | ||
|
|
18e8f07572 | ||
|
|
8aefbac223 | ||
|
|
40c6b567e3 | ||
|
|
907853b985 | ||
|
|
31fe77848f | ||
|
|
7edee84306 | ||
|
|
1e65098819 | ||
|
|
4fdf7ca7fd | ||
|
|
a7b77672e6 | ||
|
|
6d8499ea6d | ||
|
|
6d698eab08 | ||
|
|
1937527249 | ||
|
|
e03128a8de | ||
|
|
fefbcc0b4c | ||
|
|
b312895e77 | ||
|
|
e2981d2743 | ||
|
|
ae9bc914d6 | ||
|
|
6167702345 | ||
|
|
7e9ff62d89 | ||
|
|
a8b7e33010 | ||
|
|
a2de48bdfb | ||
|
|
d1640cf10f | ||
|
|
45944ddde7 | ||
|
|
52369bdf47 | ||
|
|
254cf83310 | ||
|
|
bca5edf108 | ||
|
|
b04964f4b7 | ||
|
|
63c466d821 | ||
|
|
224b0fc2ce | ||
|
|
14b213adec | ||
|
|
f0c81bcef5 | ||
|
|
128fa7f887 | ||
|
|
11e56dbdf9 | ||
|
|
c60f364774 | ||
|
|
45c2c72b60 | ||
|
|
d3d471c0a5 | ||
|
|
8455b7bece | ||
|
|
7275843c08 | ||
|
|
65854c4946 | ||
|
|
211f56c043 | ||
|
|
fc6a10f486 | ||
|
|
63fe2893a8 | ||
|
|
43dcc97512 | ||
|
|
ce2481bbe8 | ||
|
|
e05157f8e0 | ||
|
|
0e854f9534 | ||
|
|
cdef709e8a | ||
|
|
c1ac6bb06b | ||
|
|
290a9fd434 | ||
|
|
26980ae45b | ||
|
|
f325921315 | ||
|
|
dfe184f149 | ||
|
|
ca60c8e0c6 | ||
|
|
6ab06bbf9e | ||
|
|
1b8e0725d8 | ||
|
|
7b1dbd6109 | ||
|
|
7bd473371c | ||
|
|
b9d560c584 | ||
|
|
f4bec650ed | ||
|
|
8e675a9d42 | ||
|
|
a338fad036 | ||
|
|
ee0066fae4 | ||
|
|
b02babee9e | ||
|
|
f4d91b35d7 | ||
|
|
60ace79305 | ||
|
|
d2ea180a9a | ||
|
|
55644f376b | ||
|
|
38e818bb1d | ||
|
|
d276ba4acc | ||
|
|
f6077d06c4 | ||
|
|
0c2e4d0b72 | ||
|
|
617014ffbb | ||
|
|
eeb6b60fe6 | ||
|
|
1ff4706f3a | ||
|
|
acdc8763c6 | ||
|
|
af6c4291b9 | ||
|
|
3190480c12 | ||
|
|
8a4827017e | ||
|
|
d8ac973362 | ||
|
|
a7aecf7c82 | ||
|
|
eee3d7674f | ||
|
|
f69cb7a902 | ||
|
|
02f92b3ac8 | ||
|
|
974f81791f | ||
|
|
912a98e05e | ||
|
|
42e3b8aa25 | ||
|
|
9de8d1e0fa | ||
|
|
0bf6fdae14 | ||
|
|
9b38aa4680 | ||
|
|
28748ff345 | ||
|
|
1ddab1077b | ||
|
|
1455e9551e | ||
|
|
9fd7d747c1 | ||
|
|
a4094f0a7f | ||
|
|
ee26afe65e | ||
|
|
9e7740572c | ||
|
|
205a605887 | ||
|
|
8f546f253b | ||
|
|
65d22c7dbd | ||
|
|
caf5b931b9 | ||
|
|
84760dc738 | ||
|
|
93246f0658 | ||
|
|
4489f98dc3 | ||
|
|
de19d15332 | ||
|
|
a1fe6c6f44 | ||
|
|
7edee656da | ||
|
|
498ee509da | ||
|
|
55f86df5b1 | ||
|
|
eae432818a | ||
|
|
aef3dfba13 | ||
|
|
42c8fab47f | ||
|
|
f19d800f43 | ||
|
|
137373c165 | ||
|
|
6559b9d222 | ||
|
|
4b5d4d759a | ||
|
|
e1334448ea | ||
|
|
02daaf391a | ||
|
|
2ffebb0b5e | ||
|
|
87338c994f | ||
|
|
109ded4068 | ||
|
|
3f3384b7c4 | ||
|
|
b223696079 | ||
|
|
c4164b3497 | ||
|
|
dcf5af647e | ||
|
|
1b21789444 | ||
|
|
066ccea733 | ||
|
|
d2734dfe1e | ||
|
|
e69b44d1ed | ||
|
|
767433d7a4 | ||
|
|
682275dc0a | ||
|
|
9944efbd0f | ||
|
|
7923446219 | ||
|
|
3ff804b3b2 | ||
|
|
ef96e6b8ff | ||
|
|
7f94518008 | ||
|
|
40519005f5 | ||
|
|
ea981ae955 | ||
|
|
4bceb9d9cd | ||
|
|
37e4a5dcd4 | ||
|
|
6479adb1a3 | ||
|
|
02cd520acd | ||
|
|
357a6dd467 | ||
|
|
f05e6e0529 | ||
|
|
06c634adee | ||
|
|
05cc023164 | ||
|
|
abc4e3c2d0 | ||
|
|
35a6e0fedd | ||
|
|
de453ab06d | ||
|
|
d8778e7ca9 | ||
|
|
b136eb5151 | ||
|
|
d7d5d8fcca | ||
|
|
68a2f34a46 | ||
|
|
a9bc65ee1f | ||
|
|
432e399f44 | ||
|
|
a81ab5e26b | ||
|
|
8d04d6d3ce | ||
|
|
f673144b19 | ||
|
|
eebbf3f812 | ||
|
|
3a5e588561 | ||
|
|
5101bf1253 | ||
|
|
bd33b1ae30 | ||
|
|
68e543f7e1 | ||
|
|
411057d1b4 | ||
|
|
ae99e1b02b | ||
|
|
2c46bddcfa | ||
|
|
e4bf5c1ddb | ||
|
|
6474725d4f | ||
|
|
c5677decd3 | ||
|
|
2820aedb87 | ||
|
|
9b2ef002ea | ||
|
|
a9161af593 | ||
|
|
37de65fe48 | ||
|
|
6dba652b25 | ||
|
|
d2a6c1fc62 | ||
|
|
130180cc5c | ||
|
|
72f1313cc3 | ||
|
|
a663971ce4 | ||
|
|
b23f43a7e7 | ||
|
|
a90d7009f5 | ||
|
|
4986395998 | ||
|
|
19edc1ab1d | ||
|
|
57d02195b5 | ||
|
|
8492786ae4 | ||
|
|
46e5053705 | ||
|
|
4087b020db | ||
|
|
8a2699dfbb | ||
|
|
276a2b02a3 | ||
|
|
77f24af727 | ||
|
|
6932cf6c8d | ||
|
|
5b737a0a0b | ||
|
|
a317e59df1 | ||
|
|
0daf835ae9 | ||
|
|
17c7b72cb0 | ||
|
|
0c87771064 | ||
|
|
7df06b278a | ||
|
|
51a5a4ec3b | ||
|
|
6ccae711fe | ||
|
|
ccca126749 | ||
|
|
7e6e8ee169 | ||
|
|
753aef4910 | ||
|
|
9a78c0e2fd | ||
|
|
7200bddd09 | ||
|
|
47081cb118 | ||
|
|
5e98a702a2 | ||
|
|
2361bceb70 | ||
|
|
0c8897949e | ||
|
|
8610c0c95e | ||
|
|
7821fa6866 | ||
|
|
3d5b8ecdd5 | ||
|
|
65ee446642 | ||
|
|
d305803015 | ||
|
|
2786a4e59b | ||
|
|
afcbc7e17a | ||
|
|
0bb5a8210a | ||
|
|
90ee067972 | ||
|
|
e7b3297c2d | ||
|
|
e28371f53a | ||
|
|
509ce3e16e | ||
|
|
d9ed98b951 | ||
|
|
e6ed5cc6cf | ||
|
|
cb06a91368 | ||
|
|
93d1b6dcb0 | ||
|
|
ed04a9f5ff | ||
|
|
758205a52a | ||
|
|
11ad0a9803 | ||
|
|
2e091f67b5 | ||
|
|
de1f67afbd | ||
|
|
633e126ba9 | ||
|
|
82eaa52fab | ||
|
|
06ca3b7f82 | ||
|
|
1c17d02f57 | ||
|
|
c27f6efa65 | ||
|
|
e70d5fdbc3 | ||
|
|
945a517f6c | ||
|
|
2674689442 | ||
|
|
fd6bd29303 | ||
|
|
92bbd437f3 | ||
|
|
2613e122ff | ||
|
|
b0b69ce819 | ||
|
|
4e87bf9e0e | ||
|
|
6545d2178b | ||
|
|
6984546ae2 | ||
|
|
611c1d600f | ||
|
|
5019fe147c | ||
|
|
0f13f47269 | ||
|
|
605c4af9c6 | ||
|
|
cf257f9617 | ||
|
|
493a340531 | ||
|
|
7a9512d02e | ||
|
|
2734f49b9e | ||
|
|
22dd0d47eb | ||
|
|
f8c01e7fb3 | ||
|
|
98d2ca00c0 | ||
|
|
10c5103ed7 | ||
|
|
2e79ce3318 | ||
|
|
e5145e6c55 | ||
|
|
039f196dd8 | ||
|
|
4403d66ac6 | ||
|
|
aa849ab2d5 | ||
|
|
d864caf79b | ||
|
|
fd1e6f457e | ||
|
|
f8c4001e35 | ||
|
|
b4b5781a14 | ||
|
|
0eb4e7e15f | ||
|
|
673afe20b1 | ||
|
|
793a069644 | ||
|
|
2c2295700d | ||
|
|
e9bd9599a2 | ||
|
|
b78ef78507 | ||
|
|
14075c4658 | ||
|
|
e501549bd7 | ||
|
|
13ed7a1c32 | ||
|
|
debf6cb510 | ||
|
|
8e9d8145cd | ||
|
|
90773ed4f3 | ||
|
|
3d62db5a02 | ||
|
|
8f3a7ff342 | ||
|
|
76f5514a92 | ||
|
|
614515e631 | ||
|
|
57dae7a0c1 | ||
|
|
1672cdfb06 | ||
|
|
d0139a7826 | ||
|
|
b3ff72c983 | ||
|
|
b10057a1b2 | ||
|
|
2430cb5dea | ||
|
|
e6d0d291e2 | ||
|
|
12cae708a8 | ||
|
|
d16e71d767 | ||
|
|
7b0e44e293 | ||
|
|
a7bad88159 | ||
|
|
f41ceb51da | ||
|
|
7f6324e298 | ||
|
|
3c6b52bb91 | ||
|
|
a12d81fd0a | ||
|
|
423a6163bc | ||
|
|
bfa1d3e188 | ||
|
|
fced1718cc | ||
|
|
2370cb12b3 | ||
|
|
9171b9e0ad | ||
|
|
9d14acd87f | ||
|
|
242b5b18f6 | ||
|
|
230e5c86d8 | ||
|
|
c38589bb0d | ||
|
|
4e27dedeb4 | ||
|
|
10c0ff22d6 | ||
|
|
e6a957a426 | ||
|
|
27ada0210d | ||
|
|
bb22acd4c9 | ||
|
|
6ef90f5acf | ||
|
|
fe4eba5941 | ||
|
|
ab115dfe74 | ||
|
|
b49a61d247 | ||
|
|
4ae9b7584b | ||
|
|
61a1e7f774 | ||
|
|
7fa5e694b1 | ||
|
|
6de48d30ed | ||
|
|
528dedda03 | ||
|
|
660fcfd10b | ||
|
|
a66cc0e503 | ||
|
|
25557c5882 | ||
|
|
aa3ff022f3 | ||
|
|
2a4c30892e | ||
|
|
866b3cd0ae | ||
|
|
21c1b266cf | ||
|
|
3b65181e01 | ||
|
|
80e8270954 | ||
|
|
0293f7128c | ||
|
|
dd34100310 | ||
|
|
426972e55f | ||
|
|
dee51991c3 | ||
|
|
eb05f8bd0c | ||
|
|
c6c8b80219 | ||
|
|
5d10606e09 | ||
|
|
bd1adf2c23 | ||
|
|
a4ade13c0a | ||
|
|
b251682aec | ||
|
|
d5a3b6f4f6 | ||
|
|
3db983fc2e | ||
|
|
5ace747c05 | ||
|
|
07abd1b41b | ||
|
|
576a27667f | ||
|
|
8f799210d3 | ||
|
|
a42d4f2c90 | ||
|
|
4d5faab19c | ||
|
|
bc8871eccd | ||
|
|
f791371224 | ||
|
|
b2de740556 | ||
|
|
ebd5a5a7f3 | ||
|
|
0bd0c7a61e | ||
|
|
2fb6b338bd | ||
|
|
c2def08551 | ||
|
|
10399e8eb8 | ||
|
|
4457df334b | ||
|
|
6557fed55d | ||
|
|
a65b6c7a2b | ||
|
|
44b7216861 | ||
|
|
6718622549 | ||
|
|
2f1224b27b | ||
|
|
198bf55aaf | ||
|
|
a11b5f4377 | ||
|
|
233790ced5 | ||
|
|
7168a9c50a | ||
|
|
1bc39e7e99 | ||
|
|
4f61932c71 | ||
|
|
d827fef8c4 | ||
|
|
84999f590e | ||
|
|
2742a58226 | ||
|
|
b635fdddbd | ||
|
|
a299fba15d | ||
|
|
48f8f2402a | ||
|
|
bbb3a831b5 | ||
|
|
1a7eb8fdec | ||
|
|
a0419173ba | ||
|
|
2e0b143856 | ||
|
|
003b5c392a | ||
|
|
49039968f7 | ||
|
|
ad6e58a457 | ||
|
|
f6f931be87 | ||
|
|
da6399dc3c | ||
|
|
69c1aa5bbf | ||
|
|
6364c9ee62 | ||
|
|
d06bd6abd8 | ||
|
|
ec0c39a28e | ||
|
|
ee0193ccd3 | ||
|
|
ae3455141c | ||
|
|
058e19fed0 | ||
|
|
e9caa44b55 | ||
|
|
cb38b1a0f1 | ||
|
|
000c524983 | ||
|
|
6e61236fb7 | ||
|
|
fc92266114 | ||
|
|
abbe29e58e | ||
|
|
d863c46859 | ||
|
|
66ad722964 | ||
|
|
7526eb1a2f | ||
|
|
f02c804cad | ||
|
|
a4a31b3113 | ||
|
|
555258c75d | ||
|
|
ba2c84bade | ||
|
|
e41aa00c20 | ||
|
|
b99c5e4aad | ||
|
|
5f8b1f7f80 | ||
|
|
d2d887bb50 | ||
|
|
482dfaf998 | ||
|
|
e36cc8c9d2 | ||
|
|
a148d16a95 | ||
|
|
f4e0079ec1 | ||
|
|
b451de5496 | ||
|
|
ef1b06ddf9 | ||
|
|
0b1d1f5ce4 | ||
|
|
d942542c84 | ||
|
|
a99fa4e4e5 | ||
|
|
a6d55e3252 | ||
|
|
8a560ecaba | ||
|
|
bdf665a34a | ||
|
|
c13b24fb40 | ||
|
|
aa86eab2cc | ||
|
|
5dfcda54f9 | ||
|
|
5af7a9d850 | ||
|
|
6958901d98 | ||
|
|
cb64491512 | ||
|
|
8d8a1c0f0c | ||
|
|
7c9e0345a0 | ||
|
|
df1ba08b94 | ||
|
|
2305549460 | ||
|
|
10a7b9d9a8 | ||
|
|
04161ad6f2 | ||
|
|
503401dde5 | ||
|
|
81029f8f75 | ||
|
|
1eb82ac707 | ||
|
|
e848bcb9d6 | ||
|
|
39bf7c0d43 | ||
|
|
8dd2dbb46e | ||
|
|
0be5046a72 | ||
|
|
91e024c8fa | ||
|
|
b2b3587c0b | ||
|
|
1d891a6e77 | ||
|
|
4117206c67 | ||
|
|
ec2c8ad8bc | ||
|
|
f80626b5d0 | ||
|
|
de358d661a | ||
|
|
22ed9cb5dc | ||
|
|
205077e3a0 | ||
|
|
2fe9b3a967 | ||
|
|
5ac849377a | ||
|
|
0a0b4e019e | ||
|
|
d020c3d8b0 | ||
|
|
b756f77787 | ||
|
|
46e0616614 | ||
|
|
39ef3dba1a | ||
|
|
430b9bbdc0 | ||
|
|
72812b519e | ||
|
|
eb97e0b393 | ||
|
|
ee022eada0 | ||
|
|
9de0643d97 | ||
|
|
033ef88a4e | ||
|
|
7d65002bf0 | ||
|
|
19acded31f | ||
|
|
65c0c7e60d | ||
|
|
c2dae4b49c | ||
|
|
6f8ac0c1b1 | ||
|
|
566cc6c862 | ||
|
|
3987086275 | ||
|
|
efcddb6f5b | ||
|
|
74c5af1a84 | ||
|
|
cf013576d4 | ||
|
|
1fb79fa6e9 | ||
|
|
3c73e94b4e | ||
|
|
1b7eda2745 | ||
|
|
de1de68f90 | ||
|
|
263e57e5e6 | ||
|
|
6ad55ec3b0 | ||
|
|
2b818e203f | ||
|
|
4d05ad2789 | ||
|
|
7f5e4a9fe0 | ||
|
|
c3628c87d5 | ||
|
|
0bd433d153 | ||
|
|
4d49b6ba1d | ||
|
|
cf08529362 | ||
|
|
34d3261e58 | ||
|
|
bdd1d61803 | ||
|
|
6ebd053c0f | ||
|
|
422ff579f4 | ||
|
|
d34aa4471b | ||
|
|
df22dd4def | ||
|
|
98f6d56e8c | ||
|
|
02a73a0d9b | ||
|
|
74bbc3bc04 | ||
|
|
083a630b44 | ||
|
|
50206d44dc | ||
|
|
d081627501 | ||
|
|
fcb7681683 | ||
|
|
cd3ef1850d | ||
|
|
c5eb13c114 | ||
|
|
d3f925ae63 | ||
|
|
968e74890d | ||
|
|
aa7119a741 | ||
|
|
f130e18e2c | ||
|
|
fadf60eea6 | ||
|
|
eb252e055b | ||
|
|
4509f82bc3 | ||
|
|
a9bdaec63c | ||
|
|
5883d3dabf | ||
|
|
a0bdaa4c9e | ||
|
|
1f1fb03138 | ||
|
|
d160f83717 | ||
|
|
36c64c66db | ||
|
|
d1f172f332 | ||
|
|
18446996ab | ||
|
|
c2f5e35f1d | ||
|
|
a4b55a00ec | ||
|
|
7d249d23e1 | ||
|
|
388fcd6d32 | ||
|
|
48326620ec | ||
|
|
6753e7b1e6 | ||
|
|
ed0bc38e15 | ||
|
|
2d2ba0addf | ||
|
|
d2f81da696 | ||
|
|
97a29a1f7e | ||
|
|
d8e6e34c34 | ||
|
|
4b16252c48 | ||
|
|
7b94181a0d | ||
|
|
d603048698 | ||
|
|
930b6cf45e | ||
|
|
5bddfa7a92 | ||
|
|
4918c56ca3 | ||
|
|
0fcbfd04ce | ||
|
|
d386237d86 | ||
|
|
bc9a94b5ee | ||
|
|
4ada384fcd | ||
|
|
008a531535 | ||
|
|
e170852e97 | ||
|
|
8fca139246 | ||
|
|
289797e7eb | ||
|
|
799d902c98 | ||
|
|
1a26d3b60f | ||
|
|
0be0931fcd | ||
|
|
3228b476fa | ||
|
|
410a8f8aeb | ||
|
|
7d6ecd1ccb | ||
|
|
e5d35cb3ce | ||
|
|
cfbdccdbed | ||
|
|
8f464c2a07 | ||
|
|
c9b0593220 | ||
|
|
b9e221a49c | ||
|
|
42784816c3 | ||
|
|
f949b31976 | ||
|
|
7993600019 | ||
|
|
fe6d51fa77 | ||
|
|
b865a48b92 | ||
|
|
45b97b14c4 | ||
|
|
9c934c8abf | ||
|
|
cb5b5ed334 | ||
|
|
b0f83f1486 | ||
|
|
144f10d8cb | ||
|
|
6bd53273da | ||
|
|
d5290e2a5c | ||
|
|
2d1e32748c | ||
|
|
acd60ac1cf | ||
|
|
37b9d32e04 | ||
|
|
88fb097275 | ||
|
|
6869ee65e0 | ||
|
|
63a3c678cf | ||
|
|
e7626b8500 | ||
|
|
6781a534f9 | ||
|
|
26c4845157 | ||
|
|
ba63900b29 | ||
|
|
cabe619d97 | ||
|
|
8e756f0452 | ||
|
|
de9ff82b9c | ||
|
|
9cafef1fdd | ||
|
|
482473aa69 | ||
|
|
f18bfdc674 | ||
|
|
8da898d483 | ||
|
|
ebb0243e87 | ||
|
|
58cc31cbe4 | ||
|
|
674541f922 | ||
|
|
811fc5317c | ||
|
|
09223442f5 | ||
|
|
a09bfaf9db | ||
|
|
876b659c2c | ||
|
|
69e91f2bf9 | ||
|
|
563f75ebf7 | ||
|
|
a27910d71d | ||
|
|
efa655ecf1 | ||
|
|
ff094b2560 | ||
|
|
f65237761f | ||
|
|
2c9b763906 | ||
|
|
26d85f1a52 | ||
|
|
d55427dbd9 | ||
|
|
e393e29ef5 |
+19
-5
@@ -1,5 +1,19 @@
|
||||
languages:
|
||||
Python: true
|
||||
|
||||
exclude_paths:
|
||||
- tests/
|
||||
version: "2"
|
||||
checks:
|
||||
argument-count:
|
||||
config:
|
||||
threshold: 8
|
||||
complex-logic:
|
||||
config:
|
||||
threshold: 6
|
||||
file-lines:
|
||||
config:
|
||||
threshold: 350
|
||||
method-complexity:
|
||||
config:
|
||||
threshold: 15
|
||||
method-count:
|
||||
config:
|
||||
threshold: 30
|
||||
exclude_patterns:
|
||||
- tests
|
||||
|
||||
@@ -4,10 +4,6 @@ test_patterns = [
|
||||
'tests/**'
|
||||
]
|
||||
|
||||
exclude_patterns = [
|
||||
'dvc/utils/compat.py'
|
||||
]
|
||||
|
||||
[[analyzers]]
|
||||
name = 'python'
|
||||
enabled = true
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
E203, # Whitespace before ':'
|
||||
E266, # Too many leading '#' for block comment
|
||||
W503, # Line break occurred before a binary operator
|
||||
max-line-length = 79
|
||||
select = B,C,E,F,W,T4,B9
|
||||
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: DVCorg
|
||||
@@ -1,10 +1,18 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
name: "\U0001F41B Bug Report"
|
||||
about: Create a bug report to help us improve DVC
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Please provide information about your setup**
|
||||
DVC version(i.e. `dvc --version`), Platform and method of installation (pip, homebrew, pkg Mac, exe (Windows), DEB(Linux), RPM(Linux))
|
||||
### Please provide information about your setup
|
||||
|
||||
**Output of `dvc version`:**
|
||||
|
||||
```console
|
||||
$ dvc version
|
||||
```
|
||||
|
||||
**Additional Information (if any):**
|
||||
|
||||
If applicable, please also provide a `--verbose` output of the command, eg: `dvc add --verbose`.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "🤗 Need help?"
|
||||
url: https://dvc.org/chat
|
||||
about: If you have a question, ask us on Discord. Please join with this invite 👉
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
name: Feature request
|
||||
name: "\U0001F680 Feature Request"
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
* [ ] ❗ Have you followed the guidelines in the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) list?
|
||||
* [ ] ❗ I have followed the [Contributing to DVC](https://dvc.org/doc/user-guide/contributing/core) checklist.
|
||||
|
||||
* [ ] 📖 Check this box if this PR **does not** require [documentation](https://dvc.org/doc) updates, or if it does **and** you have created a separate PR in [dvc.org](https://github.com/iterative/dvc.org) with such updates (or at least opened an issue about it in that repo). Please link below to your PR (or issue) in the [dvc.org](https://github.com/iterative/dvc.org) repo.
|
||||
* [ ] 📖 If this PR requires [documentation](https://dvc.org/doc) updates, I have created a separate PR (or issue, at least) in [dvc.org](https://github.com/iterative/dvc.org) and linked it here.
|
||||
|
||||
* [ ] ❌ Have you checked DeepSource, CodeClimate, and other sanity checks below? We consider their findings recommendatory and don't expect everything to be addressed. Please review them carefully and fix those that actually improve code or fix bugs.
|
||||
* [ ] ❌ I will check DeepSource, CodeClimate, and other sanity checks below. (We consider them recommendatory and don't expect everything to be addressed. Please fix things that actually improve code or fix bugs.)
|
||||
|
||||
Thank you for the contribution - we'll try to review it as soon as possible. 🙏
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Lint Python
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install requirements
|
||||
run: bash ./scripts/ci/install.sh
|
||||
- name: Check Patch
|
||||
run: ./scripts/ci/check_patch.sh
|
||||
+1
-1
@@ -26,7 +26,7 @@ innosetup/config.ini
|
||||
.coverage
|
||||
.coverage.*
|
||||
|
||||
*.swp
|
||||
*.sw?
|
||||
|
||||
pip-wheel-metadata/
|
||||
.vscode/
|
||||
|
||||
+72
-8
@@ -1,11 +1,75 @@
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 19.3b0
|
||||
- hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 19.10b0
|
||||
- repo: https://github.com/asottile/seed-isort-config
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
- id: seed-isort-config
|
||||
- hooks:
|
||||
- id: isort
|
||||
language_version: python3
|
||||
repo: https://github.com/timothycrosley/isort
|
||||
rev: 4.3.21
|
||||
- hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-bugbear
|
||||
- flake8-comprehensions
|
||||
- flake8-debugger
|
||||
- flake8-string-format
|
||||
repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.7.9
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint
|
||||
language: system
|
||||
types: [python]
|
||||
- hooks:
|
||||
- args:
|
||||
- -i
|
||||
- "2"
|
||||
id: beautysh
|
||||
language_version: python3
|
||||
repo: https://github.com/lovesegfault/beautysh
|
||||
rev: master
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
- hooks:
|
||||
- args:
|
||||
- git-hook
|
||||
- pre-commit
|
||||
entry: dvc
|
||||
id: dvc-pre-commit
|
||||
language: system
|
||||
name: DVC pre-commit
|
||||
stages:
|
||||
- commit
|
||||
verbose: true
|
||||
require_serial: true
|
||||
- args:
|
||||
- git-hook
|
||||
- pre-push
|
||||
entry: dvc
|
||||
id: dvc-pre-push
|
||||
language: system
|
||||
name: DVC pre-push
|
||||
stages:
|
||||
- push
|
||||
require_serial: true
|
||||
- always_run: true
|
||||
args:
|
||||
- git-hook
|
||||
- post-checkout
|
||||
entry: dvc
|
||||
id: dvc-post-checkout
|
||||
language: system
|
||||
minimum_pre_commit_version: 2.2.0
|
||||
name: DVC post-checkout
|
||||
stages:
|
||||
- post-checkout
|
||||
require_serial: true
|
||||
repo: local
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
- args:
|
||||
- git-hook
|
||||
- pre-commit
|
||||
entry: dvc
|
||||
id: dvc-pre-commit
|
||||
language: python
|
||||
language_version: python3
|
||||
name: DVC pre-commit
|
||||
require_serial: true
|
||||
stages:
|
||||
- commit
|
||||
verbose: true
|
||||
- args:
|
||||
- git-hook
|
||||
- pre-push
|
||||
entry: dvc
|
||||
id: dvc-pre-push
|
||||
language: python
|
||||
language_version: python3
|
||||
name: DVC pre-push
|
||||
require_serial: true
|
||||
stages:
|
||||
- push
|
||||
- always_run: true
|
||||
args:
|
||||
- git-hook
|
||||
- post-checkout
|
||||
entry: dvc
|
||||
id: dvc-post-checkout
|
||||
language: python
|
||||
language_version: python3
|
||||
minimum_pre_commit_version: 2.2.0
|
||||
name: DVC post-checkout
|
||||
require_serial: true
|
||||
stages:
|
||||
- post-checkout
|
||||
@@ -15,7 +15,11 @@ ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
init-hook="import sys; \
|
||||
from pathlib import Path; \
|
||||
from pylint.config import find_pylintrc; \
|
||||
sys.path.append(Path(find_pylintrc()).parent / 'tests');"
|
||||
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
@@ -28,7 +32,7 @@ limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=
|
||||
load-plugins=pylint_pytest,pylint_plugin_disable
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
@@ -60,7 +64,16 @@ confidence=
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=print-statement,
|
||||
disable=format,
|
||||
# ^ most of them are covered by flake8,
|
||||
# and are conventions rather than warnings or errors
|
||||
refactoring, # will be noisy if it starts giving suggestions
|
||||
spelling,
|
||||
# design is too-few-* and too-many-* checks, too-much-* for us right now
|
||||
design,
|
||||
# following were generated by default
|
||||
# most of them are redundant Py3 checks for Py2+3 codebase
|
||||
print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
@@ -139,9 +152,33 @@ disable=print-statement,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape,
|
||||
useless-object-inheritance,
|
||||
missing-docstring,
|
||||
bad-continuation
|
||||
# custom disables start here
|
||||
no-self-use,
|
||||
invalid-name,
|
||||
# already a check enabled for `wildcard-import`
|
||||
unused-wildcard-import,
|
||||
misplaced-comparison-constant,
|
||||
# probably soon: https://github.com/iterative/dvc/issues/1843
|
||||
logging-format-interpolation,
|
||||
logging-fstring-interpolation,
|
||||
# we don't have proper docstrings yet
|
||||
# even if we do, we'll be doing it gradually
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
duplicate-code,
|
||||
# too noisy, and detects issues even when import is inside a function
|
||||
# see: https://github.com/PyCQA/pylint/issues/850
|
||||
cyclic-import,
|
||||
# isort should cover these:
|
||||
wrong-import-order,
|
||||
wrong-import-position,
|
||||
ungrouped-imports,
|
||||
multiple-imports,
|
||||
# we use it to speedup/optimize
|
||||
import-outside-toplevel,
|
||||
fixme,
|
||||
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
@@ -228,7 +265,7 @@ contextmanager-decorators=contextlib.contextmanager
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
generated-members=pytest.lazy_fixture,logging.TRACE,logger.trace,sys.getwindowsversion
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
@@ -249,7 +286,7 @@ ignore-on-opaque-inference=yes
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,Dvcfile
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
@@ -290,7 +327,7 @@ dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
ignored-argument-names=_.*|^ignored_|^unused_|args|kwargs
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
+10
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
restylers:
|
||||
- name: black
|
||||
image: restyled/restyler-black:v19.3b0
|
||||
image: restyled/restyler-black:v19.10b0
|
||||
command:
|
||||
- black
|
||||
arguments: []
|
||||
@@ -9,3 +9,12 @@ restylers:
|
||||
- "**/*.py"
|
||||
interpreters:
|
||||
- python
|
||||
- name: isort
|
||||
image: restyled/restyler-isort:v4.3.21
|
||||
command:
|
||||
- isort
|
||||
arguments: []
|
||||
include:
|
||||
- "**/*.py"
|
||||
interpreters:
|
||||
- python
|
||||
|
||||
+62
-44
@@ -13,8 +13,6 @@ stages:
|
||||
- check
|
||||
- test
|
||||
- build
|
||||
- name: deploy
|
||||
if: (branch = master OR tag is present) AND repo = iterative/dvc
|
||||
# Travis doesn't support testing python on Mac yet, so we need
|
||||
# to workaround it by installing it directly with brew.
|
||||
#
|
||||
@@ -31,7 +29,7 @@ jobs:
|
||||
os: linux
|
||||
language: python
|
||||
python: 3.7
|
||||
before_install:
|
||||
before_install: bash ./scripts/ci/install.sh
|
||||
install:
|
||||
script: ./scripts/ci/check_patch.sh
|
||||
# test jobs
|
||||
@@ -39,15 +37,24 @@ jobs:
|
||||
stage: test
|
||||
os: windows # Windows 10.0.17134 N/A Build 17134
|
||||
language: shell # 'language: python' is an error on Travis CI Windows
|
||||
- name: "3.5 on Linux"
|
||||
language: python
|
||||
python: 3.5
|
||||
env:
|
||||
- PYTHON_VERSION=3.7.5
|
||||
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
|
||||
- name: "3.8 on Windows"
|
||||
os: windows # Windows 10.0.17134 N/A Build 17134
|
||||
language: shell # 'language: python' is an error on Travis CI Windows
|
||||
env:
|
||||
- PYTHON_VERSION=3.8.0
|
||||
- PATH=/c/Python38:/c/Python38/Scripts:$PATH
|
||||
- name: "3.6 on Linux"
|
||||
language: python
|
||||
python: 3.6
|
||||
- name: "3.7 on Linux"
|
||||
language: python
|
||||
python: 3.7
|
||||
- name: "3.8 on Linux"
|
||||
language: python
|
||||
python: 3.8
|
||||
- name: "3.7 on Mac"
|
||||
os: osx
|
||||
osx_image: xcode11.3
|
||||
@@ -65,66 +72,45 @@ jobs:
|
||||
language: shell
|
||||
install:
|
||||
script: ./scripts/build_windows.cmd
|
||||
env:
|
||||
- PYTHON_VERSION=3.7.5
|
||||
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
|
||||
- name: "Linux pkgs"
|
||||
language: python
|
||||
python: 3.7
|
||||
before_install:
|
||||
install:
|
||||
script: ./scripts/build_posix.sh
|
||||
- name: "Pypi pkgs"
|
||||
- name: "PyPI pkgs"
|
||||
language: python
|
||||
python: 3.7
|
||||
python: 3.8
|
||||
script: ./scripts/build_package.sh
|
||||
# deploy jobs
|
||||
- name: "Pypi pkgs"
|
||||
if: tag is present
|
||||
stage: deploy
|
||||
language: python
|
||||
python: 3.7
|
||||
script: ./scripts/build_package.sh
|
||||
deploy:
|
||||
- provider: pypi
|
||||
user:
|
||||
secure: "IOzNfgPoG3ICg3+s4AuZCCnawgd/KqMzeiAB9R0B9Xb6ProPQJp9O3GBZrtg5OJMHdWQQtwULeOh/LWk4sFdXMcX1iNEgm5Tb7DwO7TDtltW14jUgN9AqfY2twj4UIsW/ON8w0P8v36ZoQq1kO6Pfgjijdm3w+9h6rpps8rJ6jXYpVZVO+SSzzijWddanMkEz4GgctZ2r7U5MWlmm1UogF60kEZIZCQykz0yJnhTpP9nMFDKw6Maki/mcfVwTQ/v3RqjzttHei/aaEMGQXYr3TOX8NPYJXerBpjShDZqcgcCfqPzhi7uCwVYaPvRsMMYH5jgtMz/SO3J6ToeGKT0f1F1ihRyCdusg325TMK7Vv8yHJzL4ngeUbb/oqxWtxhLY7f77Lkx6hBwe6ROpTqiQHIfZad5qyIHihKcDcIHo4FwLHVzxR4y+Ds2js5dXwBkUBaEAx0Dmc6ZdgXqxd815GDwv+VVpzXJhtO6qXmKE36F/Oe6eCNG3dk5MdOK4C2DrM9M8MXllqeQtX6JrM2stWlBalAc9Jn4b0xxAoghX3x3G3hNsLc9Uist1VinY2+IcPozusqj66c+Vbi5YngWSbICOjlIcq2GaOlBi3xioGts396442ztf1eLjMVuvykCI3b4Yz/vJYZOZjUvG/6nLfaFQTJLSXkTvI4do3foeY4="
|
||||
password:
|
||||
secure: "ts/3FSPL+w4euXryip9wCJWUqUwuJ2CqHi0nEpYVM10KL0cp9aMkQXN/7C8ih+j/JZ25kaO+tis1nx0JERVnKrg/1F/fJhxHTpakCP4OSIlhKRXDt7yUUHcKKt5QdM2XCKhApptP6RTpWL1vlc36CMrhdvTcMYp7hlDOiWjdhhim24FGQzabL42Ko59ttyyCROqgcnjHqj+WlhJl7iYO+lv2PLOtslpkIYY5EMjgBLuGyOvxTqZsAXtworsASdB2CrsTE6iMRcrM1afgkyBlUqDezoiv9TwfZrZ85oRoAxPB7su4QWK7lEP0wGQV/SDz/RL7c+cNfRaauxUKVcC8I2LfaEpudv8vi205+v/08wzPbS4xSOnf89irUJnyGxUy+bYLoXPPVPMyzcQRAUnrQl+ENCmN62HU+/2pkIyiUJvt0wYYP0vp+3pIbXbYQGmAIMq7nZJd7K3rikfnQIGKI03xZsFe3PEdjzhDI9Ej/tRUbJx4pgg084QuYYca6MSYn/zJ5pqIG4A7ngCSxoxoWZT0duo8PrUWQeT17L73Q86HeEv3OzZhWfEF2fNDN3Qbsu4CllSZnbE7DXgAuysTwTYHS/na6lp7wejOqdRUXrHbidVla9Mwxyc9JdWNWzUDxALvNxAdNbV6l85mO8izy8wjQF6M1O7idqTfjQeR4pc="
|
||||
skip_existing: true
|
||||
file_glob: true
|
||||
file:
|
||||
- dist/dvc*.whl
|
||||
- dist/dvc*.tar.gz
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
repo: iterative/dvc
|
||||
- name: Snapcraft snap
|
||||
stage: deploy
|
||||
language: python
|
||||
python: 3.8
|
||||
addons:
|
||||
snaps:
|
||||
- name: snapcraft
|
||||
channel: stable
|
||||
confinement: classic
|
||||
- name: review-tools
|
||||
channel: stable
|
||||
- name: lxd
|
||||
channel: stable
|
||||
env:
|
||||
- SNAPCRAFT_IMAGE_INFO: '{"build_url": "$TRAVIS_BUILD_URL"}'
|
||||
before_install:
|
||||
- SNAPCRAFT_IMAGE_INFO: |
|
||||
'{"build_url": "$TRAVIS_JOB_WEB_URL"}'
|
||||
- SNAPCRAFT_BUILD_ENVIRONMENT: lxd
|
||||
- SNAPCRAFT_BUILD_INFO: 1 # https://snapcraft.io/blog/introducing-developer-notifications-for-snap-security-updates
|
||||
install:
|
||||
- sudo usermod --append --groups lxd $USER
|
||||
- sudo /snap/bin/lxd.migrate -yes
|
||||
- sudo /snap/bin/lxd waitready
|
||||
- sudo /snap/bin/lxd init --auto
|
||||
install:
|
||||
script:
|
||||
- sudo snapcraft --use-lxd
|
||||
- ./scripts/build_snap.sh
|
||||
after_failure:
|
||||
- sudo journalctl -u snapd
|
||||
deploy:
|
||||
- provider: snap
|
||||
snap: dvc_*.snap
|
||||
channel: ${SOURCE_TAG:+stable}${SOURCE_TAG:-edge}
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
repo: iterative/dvc
|
||||
|
||||
before_install:
|
||||
- bash ./scripts/ci/before_install.sh
|
||||
@@ -136,7 +122,6 @@ script:
|
||||
- bash ./scripts/ci/script.sh
|
||||
deploy:
|
||||
- provider: releases
|
||||
condition: "[[ $(./scripts/ci/deploy_condition.sh) == 'true' ]]"
|
||||
api_key:
|
||||
secure: nyo17gF4Xl6DFL/KVkMewC805Cv+eK99L7SiYCU+WUAYDIzzFL3YJm7fm8bznPeor7DmpO7uSLSveMnJETBtdurW6JWhCTUss4yD66yFzpk6Z5cvHf/1UA5pdMHkkMM54fRaTRt6L2Yal7RPwGEQTBECLXW1Xmf78Y6+0l8lTvXQ9jXOQSRO/63VDKI5Zww2+XytVicUFlr0hLMGKUuaCjjXcQlAPzBgQz3/5Iz6EtzeTf2o+gTPjocbRbSFPikfiBXUn8XlJ8jBj7i114XJRJffw59KxYGDzxp+enfLBULGNoW0Np2hZqkKkPUIdB08C8XVbRXlFYX8cEYBczjqhv8A05p5F5CnltnqQ0KazcoUP2awPVqn68dWNRF+kqrnMBus8euYKIZ4dy/6bVEKJRa84ESJXZVDcwXTAXRqyovSxOlXqg9ZgAImMKBfKn/fpDX2xIvQPaXcnJxKbmcePfdbC1d0HYJ5jkbNaxKE/T15VvhzIho7mJ2WAv1r8QouQUuzqtpzIGbRn/KXHRUzCtyP0k4+eMKMLFOPzg848WsDarP3v8VYiSy4nFY7ZP7DRd3KttvW5bgVbSeO4jPmTgS6TnnpRwual9BJXSG7bjbfpIMii3bNZ7jWL4zpKT9Jfm8w9RGKLcX9HqkZ3+y60+LtYdhEfBvqsIlWQoIJBXg=
|
||||
file_glob: true
|
||||
@@ -147,6 +132,39 @@ deploy:
|
||||
- dvc*.exe
|
||||
skip_cleanup: true
|
||||
on:
|
||||
condition: "$(./scripts/ci/deploy_condition.sh dvc*.rpm dvc*.deb dvc*.pkg dvc*.exe)"
|
||||
tags: true
|
||||
repo: iterative/dvc
|
||||
stage: build
|
||||
- provider: pypi
|
||||
user:
|
||||
secure: "IOzNfgPoG3ICg3+s4AuZCCnawgd/KqMzeiAB9R0B9Xb6ProPQJp9O3GBZrtg5OJMHdWQQtwULeOh/LWk4sFdXMcX1iNEgm5Tb7DwO7TDtltW14jUgN9AqfY2twj4UIsW/ON8w0P8v36ZoQq1kO6Pfgjijdm3w+9h6rpps8rJ6jXYpVZVO+SSzzijWddanMkEz4GgctZ2r7U5MWlmm1UogF60kEZIZCQykz0yJnhTpP9nMFDKw6Maki/mcfVwTQ/v3RqjzttHei/aaEMGQXYr3TOX8NPYJXerBpjShDZqcgcCfqPzhi7uCwVYaPvRsMMYH5jgtMz/SO3J6ToeGKT0f1F1ihRyCdusg325TMK7Vv8yHJzL4ngeUbb/oqxWtxhLY7f77Lkx6hBwe6ROpTqiQHIfZad5qyIHihKcDcIHo4FwLHVzxR4y+Ds2js5dXwBkUBaEAx0Dmc6ZdgXqxd815GDwv+VVpzXJhtO6qXmKE36F/Oe6eCNG3dk5MdOK4C2DrM9M8MXllqeQtX6JrM2stWlBalAc9Jn4b0xxAoghX3x3G3hNsLc9Uist1VinY2+IcPozusqj66c+Vbi5YngWSbICOjlIcq2GaOlBi3xioGts396442ztf1eLjMVuvykCI3b4Yz/vJYZOZjUvG/6nLfaFQTJLSXkTvI4do3foeY4="
|
||||
password:
|
||||
secure: "ts/3FSPL+w4euXryip9wCJWUqUwuJ2CqHi0nEpYVM10KL0cp9aMkQXN/7C8ih+j/JZ25kaO+tis1nx0JERVnKrg/1F/fJhxHTpakCP4OSIlhKRXDt7yUUHcKKt5QdM2XCKhApptP6RTpWL1vlc36CMrhdvTcMYp7hlDOiWjdhhim24FGQzabL42Ko59ttyyCROqgcnjHqj+WlhJl7iYO+lv2PLOtslpkIYY5EMjgBLuGyOvxTqZsAXtworsASdB2CrsTE6iMRcrM1afgkyBlUqDezoiv9TwfZrZ85oRoAxPB7su4QWK7lEP0wGQV/SDz/RL7c+cNfRaauxUKVcC8I2LfaEpudv8vi205+v/08wzPbS4xSOnf89irUJnyGxUy+bYLoXPPVPMyzcQRAUnrQl+ENCmN62HU+/2pkIyiUJvt0wYYP0vp+3pIbXbYQGmAIMq7nZJd7K3rikfnQIGKI03xZsFe3PEdjzhDI9Ej/tRUbJx4pgg084QuYYca6MSYn/zJ5pqIG4A7ngCSxoxoWZT0duo8PrUWQeT17L73Q86HeEv3OzZhWfEF2fNDN3Qbsu4CllSZnbE7DXgAuysTwTYHS/na6lp7wejOqdRUXrHbidVla9Mwxyc9JdWNWzUDxALvNxAdNbV6l85mO8izy8wjQF6M1O7idqTfjQeR4pc="
|
||||
skip_existing: true
|
||||
file_glob: true
|
||||
file:
|
||||
- dist/dvc*.whl
|
||||
- dist/dvc*.tar.gz
|
||||
skip_cleanup: true
|
||||
on:
|
||||
condition: "$(./scripts/ci/deploy_condition.sh dist/dvc*.whl dist/dvc*.tar.gz)"
|
||||
tags: true
|
||||
repo: iterative/dvc
|
||||
stage: build
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: "(echo $SNAP_TOKEN | snapcraft login --with -) && timeout 600 snapcraft push dvc_*.snap --release $SNAP_CHANNEL || echo timed out"
|
||||
on:
|
||||
all_branches: true
|
||||
condition: "$(./scripts/ci/deploy_condition.sh dvc_*.snap) && ($TRAVIS_BRANCH = master || -n $TRAVIS_TAG) && -n $SNAP_CHANNEL"
|
||||
repo: iterative/dvc
|
||||
stage: build
|
||||
- provider: script
|
||||
skip_cleanup: true
|
||||
script: "(echo $SNAP_TOKEN | snapcraft login --with -) && timeout 600 snapcraft push dvc_*.snap --release $SNAP_CHANNEL_MAJOR || echo timed out"
|
||||
on:
|
||||
all_branches: true
|
||||
condition: "$(./scripts/ci/deploy_condition.sh dvc_*.snap) && ($TRAVIS_BRANCH = master || -n $TRAVIS_TAG) && -n $SNAP_CHANNEL_MAJOR"
|
||||
repo: iterative/dvc
|
||||
stage: build
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"title": "DVC: Data Version Control - Git for Data & Models",
|
||||
"keywords": [
|
||||
"data-science", "data-version-control", "machine-learning", "git",
|
||||
"developer-tools", "reproducibility", "collaboration", "ai", "python"],
|
||||
"contributors": [
|
||||
{"name": "DVC team", "type": "Other", "affiliation": "Iterative"}]
|
||||
}
|
||||
+131
-71
@@ -1,6 +1,4 @@
|
||||
.. image:: https://dvc.org/static/img/logo-github-readme.png
|
||||
:target: https://dvc.org
|
||||
:alt: DVC logo
|
||||
|Banner|
|
||||
|
||||
`Website <https://dvc.org>`_
|
||||
• `Docs <https://dvc.org/doc>`_
|
||||
@@ -10,29 +8,9 @@
|
||||
• `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
|
||||
:target: https://travis-ci.com/iterative/dvc
|
||||
:alt: Travis
|
||||
|Release| |CI| |Maintainability| |Coverage| |Donate| |DOI|
|
||||
|
||||
.. image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
.. image:: https://codecov.io/gh/iterative/dvc/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/iterative/dvc
|
||||
:alt: Codecov
|
||||
|
||||
.. image:: https://img.shields.io/badge/patreon-donate-green.svg
|
||||
:target: https://www.patreon.com/DVCorg/overview
|
||||
:alt: Donate
|
||||
|
||||
.. image:: https://anaconda.org/conda-forge/dvc/badges/version.svg
|
||||
:target: https://anaconda.org/conda-forge/dvc
|
||||
:alt: Conda-forge
|
||||
|
||||
.. image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
||||
:target: https://snapcraft.io/dvc
|
||||
:alt: Snapcraft
|
||||
|PyPI| |Packages| |Brew| |Conda| |Choco| |Snap|
|
||||
|
||||
|
|
||||
|
||||
@@ -75,9 +53,7 @@ to store data and model files seamlessly out of Git, while preserving almost the
|
||||
were stored in Git itself. To store and share the data cache, DVC supports multiple remotes - any cloud (S3, Azure,
|
||||
Google Cloud, etc) or any on-premise network storage (via SSH, for example).
|
||||
|
||||
.. image:: https://dvc.org/static/img/flow.gif
|
||||
:target: https://dvc.org/static/img/flow.gif
|
||||
:alt: how_dvc_works
|
||||
|Flowchart|
|
||||
|
||||
The DVC pipelines (computational graph) feature connects code and data together. It is possible to explicitly
|
||||
specify all steps required to produce a model: input dependencies including data, commands to run,
|
||||
@@ -115,9 +91,51 @@ Installation
|
||||
There are four options to install DVC: ``pip``, Homebrew, Conda (Anaconda) or an OS-specific package.
|
||||
Full instructions are `available here <https://dvc.org/doc/get-started/install>`_.
|
||||
|
||||
Snap (Snapcraft/Linux)
|
||||
----------------------
|
||||
|
||||
|Snap|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
snap install dvc --classic
|
||||
|
||||
This corresponds to the latest tagged release.
|
||||
Add ``--beta`` for the latest tagged release candidate,
|
||||
or ``--edge`` for the latest ``master`` version.
|
||||
|
||||
Choco (Chocolatey/Windows)
|
||||
--------------------------
|
||||
|
||||
|Choco|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
choco install dvc
|
||||
|
||||
Brew (Homebrew/Mac OS)
|
||||
----------------------
|
||||
|
||||
|Brew|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install dvc
|
||||
|
||||
Conda (Anaconda)
|
||||
----------------
|
||||
|
||||
|Conda|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
conda install -c conda-forge dvc
|
||||
|
||||
pip (PyPI)
|
||||
----------
|
||||
|
||||
|PyPI|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install dvc
|
||||
@@ -133,36 +151,11 @@ To install the development version, run:
|
||||
|
||||
pip install git+git://github.com/iterative/dvc
|
||||
|
||||
Homebrew
|
||||
--------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
brew install dvc
|
||||
|
||||
|
||||
Conda (Anaconda)
|
||||
----------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
conda install -c conda-forge dvc
|
||||
|
||||
Currently, this includes support for Python versions 2.7, 3.6 and 3.7.
|
||||
|
||||
Snap (Snapcraft)
|
||||
----------------
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
snap install dvc --classic
|
||||
|
||||
This corresponds to the latest tagged release.
|
||||
Add ``--edge`` for the latest ``master`` version.
|
||||
|
||||
Package
|
||||
-------
|
||||
|
||||
|Packages|
|
||||
|
||||
Self-contained packages for Linux, Windows, and Mac are available. The latest version of the packages
|
||||
can be found on the GitHub `releases page <https://github.com/iterative/dvc/releases>`_.
|
||||
|
||||
@@ -202,40 +195,43 @@ Comparison to related technologies
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
|Maintainability| |Donate|
|
||||
|
||||
Contributions are welcome! Please see our `Contributing Guide <https://dvc.org/doc/user-guide/contributing/core>`_ for more
|
||||
details.
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/0
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/0
|
||||
:alt: 0
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/0
|
||||
:alt: 0
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/1
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/1
|
||||
:alt: 1
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/1
|
||||
:alt: 1
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/2
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/2
|
||||
:alt: 2
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/2
|
||||
:alt: 2
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/3
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/3
|
||||
:alt: 3
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/3
|
||||
:alt: 3
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/4
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/4
|
||||
:alt: 4
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/4
|
||||
:alt: 4
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/5
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/5
|
||||
:alt: 5
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/5
|
||||
:alt: 5
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/6
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/6
|
||||
:alt: 6
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/6
|
||||
:alt: 6
|
||||
|
||||
.. image:: https://sourcerer.io/fame/efiop/iterative/dvc/images/7
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/7
|
||||
:alt: 7
|
||||
:target: https://sourcerer.io/fame/efiop/iterative/dvc/links/7
|
||||
:alt: 7
|
||||
|
||||
Mailing List
|
||||
============
|
||||
@@ -249,3 +245,67 @@ This project is distributed under the Apache license version 2.0 (see the LICENS
|
||||
|
||||
By submitting a pull request to this project, you agree to license your contribution under the Apache license version
|
||||
2.0 to this project.
|
||||
|
||||
Citation
|
||||
========
|
||||
|
||||
|DOI|
|
||||
|
||||
Iterative, *DVC: Data Version Control - Git for Data & Models* (2020)
|
||||
`DOI:10.5281/zenodo.012345 <https://doi.org/10.5281/zenodo.3677553>`_.
|
||||
|
||||
.. |Banner| image:: https://dvc.org/img/logo-github-readme.png
|
||||
:target: https://dvc.org
|
||||
:alt: DVC logo
|
||||
|
||||
.. |Release| image:: https://img.shields.io/badge/release-ok-brightgreen
|
||||
:target: https://travis-ci.com/iterative/dvc/branches
|
||||
:alt: Release
|
||||
|
||||
.. |CI| image:: https://img.shields.io/travis/com/iterative/dvc/master?label=dev&logo=travis
|
||||
:target: https://travis-ci.com/iterative/dvc/builds
|
||||
:alt: Travis dev branch
|
||||
|
||||
.. |Maintainability| image:: https://codeclimate.com/github/iterative/dvc/badges/gpa.svg
|
||||
:target: https://codeclimate.com/github/iterative/dvc
|
||||
:alt: Code Climate
|
||||
|
||||
.. |Coverage| image:: https://codecov.io/gh/iterative/dvc/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/iterative/dvc
|
||||
:alt: Codecov
|
||||
|
||||
.. |Donate| image:: https://img.shields.io/badge/patreon-donate-green.svg?logo=patreon
|
||||
:target: https://www.patreon.com/DVCorg/overview
|
||||
:alt: Donate
|
||||
|
||||
.. |Snap| image:: https://img.shields.io/badge/snap-install-82BEA0.svg?logo=snapcraft
|
||||
:target: https://snapcraft.io/dvc
|
||||
:alt: Snapcraft
|
||||
|
||||
.. |Choco| image:: https://img.shields.io/chocolatey/v/dvc?label=choco
|
||||
:target: https://chocolatey.org/packages/dvc
|
||||
:alt: Chocolatey
|
||||
|
||||
.. |Brew| image:: https://img.shields.io/homebrew/v/dvc?label=brew
|
||||
:target: https://formulae.brew.sh/formula/dvc
|
||||
:alt: Homebrew
|
||||
|
||||
.. |Conda| image:: https://img.shields.io/conda/v/conda-forge/dvc.svg?label=conda&logo=conda-forge
|
||||
:target: https://anaconda.org/conda-forge/dvc
|
||||
:alt: Conda-forge
|
||||
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/dvc.svg?label=pip&logo=PyPI&logoColor=white
|
||||
:target: https://pypi.org/project/dvc
|
||||
:alt: PyPI
|
||||
|
||||
.. |Packages| image:: https://img.shields.io/github/v/release/iterative/dvc?label=deb|pkg|rpm|exe&logo=GitHub
|
||||
:target: https://github.com/iterative/dvc/releases/latest
|
||||
:alt: deb|pkg|rpm|exe
|
||||
|
||||
.. |DOI| image:: https://img.shields.io/badge/DOI-10.5281/zenodo.3677553-blue.svg
|
||||
:target: https://doi.org/10.5281/zenodo.3677553
|
||||
:alt: DOI
|
||||
|
||||
.. |Flowchart| image:: https://dvc.org/img/flow.gif
|
||||
:target: https://dvc.org/img/flow.gif
|
||||
:alt: how_dvc_works
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@ dvc (0.75.b1) experimental; urgency=low
|
||||
|
||||
* First native Debian package version.
|
||||
|
||||
-- Dmitry Petrov <dmitry@dataversioncontrol.com> Sat, 07 Dec 2019 09:49:28 +0300
|
||||
-- Dmitry Petrov <dmitry@dvc.org> Sat, 07 Dec 2019 09:49:28 +0300
|
||||
|
||||
Vendored
+1
-1
@@ -1,5 +1,5 @@
|
||||
Source: dvc
|
||||
Maintainer: Dmitry Petrov <dmitry@dataversioncontrol.com>
|
||||
Maintainer: Dmitry Petrov <dmitry@dvc.org>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9)
|
||||
|
||||
Vendored
+2
-2
@@ -1,10 +1,10 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: DVC
|
||||
Upstream-Contact: Dmitry Petrov <dmitry@dataversioncontrol.com>
|
||||
Upstream-Contact: Dmitry Petrov <dmitry@dvc.org>
|
||||
Source: https://github.com/iterative/dvc
|
||||
|
||||
Files: *
|
||||
Copyright: Dmitry Petrov <dmitry@dataversioncontrol.com>
|
||||
Copyright: Dmitry Petrov <dmitry@dvc.org>
|
||||
License: Apache 2.0
|
||||
On Debian systems, the full text of the Apache 2.0
|
||||
License can be found in the file
|
||||
|
||||
@@ -3,17 +3,7 @@ DVC
|
||||
----
|
||||
Make your data science projects reproducible and shareable.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
import dvc.logger
|
||||
from dvc.version import __version__ # noqa: F401
|
||||
|
||||
|
||||
dvc.logger.setup()
|
||||
|
||||
# Ignore numpy's runtime warnings: https://github.com/numpy/numpy/pull/432.
|
||||
# We don't directly import numpy, but our dependency networkx does, causing
|
||||
# these warnings in some environments. Luckily these warnings are benign and
|
||||
# we can simply ignore them so that they don't show up when you are using dvc.
|
||||
warnings.filterwarnings("ignore", message="numpy.dtype size changed")
|
||||
warnings.filterwarnings("ignore", message="numpy.ufunc size changed")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Main entry point for dvc command line tool."""
|
||||
"""Main entry point for DVC command line tool."""
|
||||
import sys
|
||||
|
||||
from dvc.main import main
|
||||
|
||||
+21
-17
@@ -2,12 +2,12 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import requests
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import distro
|
||||
import requests
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.config import Config, to_bool
|
||||
@@ -15,11 +15,11 @@ from dvc.daemon import daemon
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.lock import Lock, LockError
|
||||
from dvc.repo import Repo
|
||||
from dvc.scm import SCM
|
||||
from dvc.scm import SCM, NoSCM
|
||||
from dvc.scm.base import SCMError
|
||||
from dvc.utils import env2bool, is_binary
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ def collect_and_send_report(args=None, return_code=None):
|
||||
report as a JSON, where the _collector_ generates it and the _sender_
|
||||
removes it after sending it.
|
||||
"""
|
||||
report = _runtime_info()
|
||||
report = {}
|
||||
|
||||
# Include command execution information on the report only when available.
|
||||
if args and hasattr(args, "func"):
|
||||
@@ -54,17 +54,14 @@ def is_enabled():
|
||||
return False
|
||||
|
||||
enabled = to_bool(
|
||||
Config(validate=False)
|
||||
.config.get(Config.SECTION_CORE, {})
|
||||
.get(Config.SECTION_CORE_ANALYTICS, "true")
|
||||
Config(validate=False).get("core", {}).get("analytics", "true")
|
||||
)
|
||||
|
||||
logger.debug("Analytics is {}abled.".format("en" if enabled else "dis"))
|
||||
|
||||
return enabled
|
||||
|
||||
|
||||
def send(report):
|
||||
def send(path):
|
||||
"""
|
||||
Side effect: Removes the report after sending it.
|
||||
|
||||
@@ -75,16 +72,25 @@ def send(report):
|
||||
url = "https://analytics.dvc.org"
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with open(report, "rb") as fobj:
|
||||
requests.post(url, data=fobj, headers=headers, timeout=5)
|
||||
with open(path) as fobj:
|
||||
report = json.load(fobj)
|
||||
|
||||
os.remove(report)
|
||||
report.update(_runtime_info())
|
||||
|
||||
try:
|
||||
requests.post(url, json=report, headers=headers, timeout=5)
|
||||
except requests.exceptions.RequestException:
|
||||
logger.debug("failed to send analytics report", exc_info=True)
|
||||
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def _scm_in_use():
|
||||
try:
|
||||
scm = SCM(root_dir=Repo.find_root())
|
||||
return type(scm).__name__
|
||||
except SCMError:
|
||||
return NoSCM.__name__
|
||||
except NotDvcRepoError:
|
||||
pass
|
||||
|
||||
@@ -141,7 +147,7 @@ def _find_or_create_user_id():
|
||||
|
||||
IDs are generated randomly with UUID.
|
||||
"""
|
||||
config_dir = Config.get_global_config_dir()
|
||||
config_dir = Config.get_dir("global")
|
||||
fname = os.path.join(config_dir, "user_id")
|
||||
lockfile = os.path.join(config_dir, "user_id.lock")
|
||||
|
||||
@@ -152,7 +158,7 @@ def _find_or_create_user_id():
|
||||
try:
|
||||
with Lock(lockfile):
|
||||
try:
|
||||
with open(fname, "r") as fobj:
|
||||
with open(fname) as fobj:
|
||||
user_id = json.load(fobj)["user_id"]
|
||||
|
||||
except (FileNotFoundError, ValueError, KeyError):
|
||||
@@ -164,6 +170,4 @@ def _find_or_create_user_id():
|
||||
return user_id
|
||||
|
||||
except LockError:
|
||||
logger.debug(
|
||||
"Failed to acquire '{lockfile}'".format(lockfile=lockfile)
|
||||
)
|
||||
logger.debug(f"Failed to acquire '{lockfile}'")
|
||||
|
||||
+56
-176
@@ -1,60 +1,53 @@
|
||||
from builtins import open as builtin_open
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
from contextlib import contextmanager, _GeneratorContextManager as GCM
|
||||
import threading
|
||||
from contextlib import _GeneratorContextManager as GCM
|
||||
from contextlib import contextmanager
|
||||
|
||||
from funcy import wrap_with
|
||||
import ruamel.yaml
|
||||
from voluptuous import Schema, Required, Invalid
|
||||
|
||||
from dvc.repo import Repo
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.external_repo import external_repo
|
||||
from dvc.repo import Repo
|
||||
|
||||
|
||||
SUMMON_FILE_SCHEMA = Schema(
|
||||
{
|
||||
Required("objects"): [
|
||||
{
|
||||
Required("name"): str,
|
||||
"meta": dict,
|
||||
Required("summon"): {
|
||||
Required("type"): str,
|
||||
"deps": [str],
|
||||
str: object,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
SUMMON_PYTHON_SCHEMA = Schema(
|
||||
{
|
||||
Required("type"): "python",
|
||||
Required("call"): str,
|
||||
"args": dict,
|
||||
"deps": [str],
|
||||
}
|
||||
)
|
||||
class UrlNotDvcRepoError(DvcException):
|
||||
"""Thrown if the given URL is not a DVC repository."""
|
||||
|
||||
|
||||
class SummonError(DvcException):
|
||||
pass
|
||||
def __init__(self, url):
|
||||
super().__init__(f"'{url}' is not a DVC repository.")
|
||||
|
||||
|
||||
def get_url(path, repo=None, rev=None, remote=None):
|
||||
"""Returns an url of a resource specified by path in repo"""
|
||||
"""
|
||||
Returns the URL to the storage location of a data file or directory tracked
|
||||
in a DVC repo. For Git repos, HEAD is used unless a rev argument is
|
||||
supplied. The default remote is tried unless a remote argument is supplied.
|
||||
|
||||
Raises UrlNotDvcRepoError if repo is not a DVC project.
|
||||
|
||||
NOTE: This function does not check for the actual existence of the file or
|
||||
directory in the remote storage.
|
||||
"""
|
||||
with _make_repo(repo, rev=rev) as _repo:
|
||||
abspath = os.path.join(_repo.root_dir, path)
|
||||
out, = _repo.find_outs_by_path(abspath)
|
||||
if not isinstance(_repo, Repo):
|
||||
raise UrlNotDvcRepoError(_repo.url) # pylint: disable=no-member
|
||||
out = _repo.find_out_by_relpath(path)
|
||||
remote_obj = _repo.cloud.get_remote(remote)
|
||||
return str(remote_obj.checksum_to_path_info(out.checksum))
|
||||
return str(remote_obj.tree.hash_to_path_info(out.checksum))
|
||||
|
||||
|
||||
def open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
"""Opens a specified resource as a file descriptor"""
|
||||
def open( # noqa, pylint: disable=redefined-builtin
|
||||
path, repo=None, rev=None, remote=None, mode="r", encoding=None
|
||||
):
|
||||
"""
|
||||
Open file in the supplied path tracked in a repo (both DVC projects and
|
||||
plain Git repos are supported). For Git repos, HEAD is used unless a rev
|
||||
argument is supplied. The default remote is tried unless a remote argument
|
||||
is supplied. It may only be used as a context manager:
|
||||
|
||||
with dvc.api.open(
|
||||
'path/to/file',
|
||||
repo='https://example.com/url/to/repo'
|
||||
) as fd:
|
||||
# ... Handle file object fd
|
||||
"""
|
||||
args = (path,)
|
||||
kwargs = {
|
||||
"repo": repo,
|
||||
@@ -67,27 +60,32 @@ def open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
|
||||
|
||||
class _OpenContextManager(GCM):
|
||||
def __init__(self, func, args, kwds):
|
||||
def __init__(
|
||||
self, func, args, kwds
|
||||
): # pylint: disable=super-init-not-called
|
||||
self.gen = func(*args, **kwds)
|
||||
self.func, self.args, self.kwds = func, args, kwds
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise AttributeError(
|
||||
"dvc.api.open() should be used in a with statement"
|
||||
"dvc.api.open() should be used in a with statement."
|
||||
)
|
||||
|
||||
|
||||
def _open(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
with _make_repo(repo, rev=rev) as _repo:
|
||||
abspath = os.path.join(_repo.root_dir, path)
|
||||
with _repo.open(
|
||||
abspath, remote=remote, mode=mode, encoding=encoding
|
||||
with _repo.open_by_relpath(
|
||||
path, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
yield fd
|
||||
|
||||
|
||||
def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
"""Read a specified resource into string"""
|
||||
"""
|
||||
Returns the contents of a tracked file (by DVC or Git). For Git repos, HEAD
|
||||
is used unless a rev argument is supplied. The default remote is tried
|
||||
unless a remote argument is supplied.
|
||||
"""
|
||||
with open(
|
||||
path, repo=repo, rev=rev, remote=remote, mode=mode, encoding=encoding
|
||||
) as fd:
|
||||
@@ -95,131 +93,13 @@ def read(path, repo=None, rev=None, remote=None, mode="r", encoding=None):
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _make_repo(repo_url, rev=None):
|
||||
if not repo_url or urlparse(repo_url).scheme == "":
|
||||
assert rev is None, "Custom revision is not supported for local repo"
|
||||
yield Repo(repo_url)
|
||||
else:
|
||||
with external_repo(url=repo_url, rev=rev) as repo:
|
||||
yield repo
|
||||
|
||||
|
||||
def summon(name, repo=None, rev=None, summon_file="dvcsummon.yaml", args=None):
|
||||
"""Instantiate an object described in the summon file."""
|
||||
with prepare_summon(
|
||||
name, repo=repo, rev=rev, summon_file=summon_file
|
||||
) as desc:
|
||||
def _make_repo(repo_url=None, rev=None):
|
||||
repo_url = repo_url or os.getcwd()
|
||||
if rev is None and os.path.exists(repo_url):
|
||||
try:
|
||||
summon_dict = SUMMON_PYTHON_SCHEMA(desc.obj["summon"])
|
||||
except Invalid as exc:
|
||||
raise SummonError(str(exc)) from exc
|
||||
|
||||
_args = {**summon_dict.get("args", {}), **(args or {})}
|
||||
return _invoke_method(summon_dict["call"], _args, desc.repo.root_dir)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def prepare_summon(name, repo=None, rev=None, summon_file="dvcsummon.yaml"):
|
||||
"""Does a couple of things every summon needs as a prerequisite:
|
||||
clones the repo, parses the summon file and pulls the deps.
|
||||
|
||||
Calling code is expected to complete the summon logic following
|
||||
instructions stated in "summon" dict of the object spec.
|
||||
|
||||
Returns a SummonDesc instance, which contains references to a Repo object,
|
||||
named object specification and resolved paths to deps.
|
||||
"""
|
||||
with _make_repo(repo, rev=rev) as _repo:
|
||||
try:
|
||||
path = os.path.join(_repo.root_dir, summon_file)
|
||||
obj = _get_object_spec(name, path)
|
||||
yield SummonDesc(_repo, obj)
|
||||
except SummonError as exc:
|
||||
raise SummonError(
|
||||
str(exc) + " at '{}' in '{}'".format(summon_file, repo)
|
||||
) from exc.__cause__
|
||||
|
||||
|
||||
class SummonDesc:
|
||||
def __init__(self, repo, obj):
|
||||
self.repo = repo
|
||||
self.obj = obj
|
||||
self._pull_deps()
|
||||
|
||||
@property
|
||||
def deps(self):
|
||||
return [os.path.join(self.repo.root_dir, d) for d in self._deps]
|
||||
|
||||
@property
|
||||
def _deps(self):
|
||||
return self.obj["summon"].get("deps", [])
|
||||
|
||||
def _pull_deps(self):
|
||||
if not self._deps:
|
||||
yield Repo(repo_url)
|
||||
return
|
||||
|
||||
outs = [self.repo.find_out_by_relpath(d) for d in self._deps]
|
||||
|
||||
with self.repo.state:
|
||||
for out in outs:
|
||||
self.repo.cloud.pull(out.get_used_cache())
|
||||
out.checkout()
|
||||
|
||||
|
||||
def _get_object_spec(name, path):
|
||||
"""
|
||||
Given a summonable object's name, search for it on the given file
|
||||
and return its description.
|
||||
"""
|
||||
try:
|
||||
with builtin_open(path, "r") as fobj:
|
||||
content = SUMMON_FILE_SCHEMA(ruamel.yaml.safe_load(fobj.read()))
|
||||
objects = [x for x in content["objects"] if x["name"] == name]
|
||||
|
||||
if not objects:
|
||||
raise SummonError("No object with name '{}'".format(name))
|
||||
elif len(objects) >= 2:
|
||||
raise SummonError(
|
||||
"More than one object with name '{}'".format(name)
|
||||
)
|
||||
|
||||
return objects[0]
|
||||
|
||||
except FileNotFoundError as exc:
|
||||
raise SummonError("Summon file not found") from exc
|
||||
except ruamel.yaml.YAMLError as exc:
|
||||
raise SummonError("Failed to parse summon file") from exc
|
||||
except Invalid as exc:
|
||||
raise SummonError(str(exc)) from exc
|
||||
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _invoke_method(call, args, path):
|
||||
# XXX: Some issues with this approach:
|
||||
# * Import will pollute sys.modules
|
||||
# * sys.path manipulation is "theoretically" not needed,
|
||||
# but tests are failing for an unknown reason.
|
||||
cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
os.chdir(path)
|
||||
sys.path.insert(0, path)
|
||||
method = _import_string(call)
|
||||
return method(**args)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
sys.path.pop(0)
|
||||
|
||||
|
||||
def _import_string(import_name):
|
||||
"""Imports an object based on a string.
|
||||
Useful to delay import to not load everything on startup.
|
||||
Use dotted notaion in `import_name`, e.g. 'dvc.remote.gs.RemoteGS'.
|
||||
|
||||
:return: imported object
|
||||
"""
|
||||
if "." in import_name:
|
||||
module, obj = import_name.rsplit(".", 1)
|
||||
else:
|
||||
return importlib.import_module(import_name)
|
||||
return getattr(importlib.import_module(module), obj)
|
||||
except NotDvcRepoError:
|
||||
pass # fallthrough to external_repo
|
||||
with external_repo(url=repo_url, rev=rev) as repo:
|
||||
yield repo
|
||||
|
||||
+91
-56
@@ -1,25 +1,8 @@
|
||||
"""Manages cache of a dvc repo."""
|
||||
import os
|
||||
"""Manages cache of a DVC repo."""
|
||||
from collections import defaultdict
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.config import Config
|
||||
|
||||
|
||||
class CacheConfig(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
def set_dir(self, dname, level=None):
|
||||
from dvc.remote.config import RemoteConfig
|
||||
|
||||
configobj = self.config.get_configobj(level)
|
||||
path = RemoteConfig.resolve_path(dname, configobj.filename)
|
||||
self.config.set(
|
||||
Config.SECTION_CACHE, Config.SECTION_CACHE_DIR, path, level=level
|
||||
)
|
||||
|
||||
|
||||
def _make_remote_property(name):
|
||||
"""
|
||||
@@ -45,20 +28,22 @@ def _make_remote_property(name):
|
||||
"""
|
||||
|
||||
def getter(self):
|
||||
from dvc.remote import Remote
|
||||
from dvc.remote import get_cloud_tree
|
||||
from dvc.remote.base import CloudCache
|
||||
|
||||
remote = self.config.get(name)
|
||||
if not remote:
|
||||
return None
|
||||
|
||||
return Remote(self.repo, name=remote)
|
||||
tree = get_cloud_tree(self.repo, name=remote)
|
||||
return CloudCache(tree)
|
||||
|
||||
getter.__name__ = name
|
||||
return cached_property(getter)
|
||||
|
||||
|
||||
class Cache(object):
|
||||
"""Class that manages cache locations of a dvc repo.
|
||||
class Cache:
|
||||
"""Class that manages cache locations of a DVC repo.
|
||||
|
||||
Args:
|
||||
repo (dvc.repo.Repo): repo instance that this cache belongs to.
|
||||
@@ -67,45 +52,63 @@ class Cache(object):
|
||||
CACHE_DIR = "cache"
|
||||
|
||||
def __init__(self, repo):
|
||||
from dvc.remote import Remote
|
||||
from dvc.remote import get_cloud_tree
|
||||
from dvc.remote.local import LocalCache
|
||||
|
||||
self.repo = repo
|
||||
self.config = config = repo.config["cache"]
|
||||
|
||||
self.config = config = repo.config.config[Config.SECTION_CACHE]
|
||||
local = config.get(Config.SECTION_CACHE_LOCAL)
|
||||
local = config.get("local")
|
||||
|
||||
if local:
|
||||
name = Config.SECTION_REMOTE_FMT.format(local)
|
||||
settings = repo.config.config[name]
|
||||
settings = {"name": local}
|
||||
else:
|
||||
default_cache_dir = os.path.join(repo.dvc_dir, self.CACHE_DIR)
|
||||
cache_dir = config.get(Config.SECTION_CACHE_DIR, default_cache_dir)
|
||||
cache_type = config.get(Config.SECTION_CACHE_TYPE)
|
||||
protected = config.get(Config.SECTION_CACHE_PROTECTED)
|
||||
shared = config.get(Config.SECTION_CACHE_SHARED)
|
||||
settings = {**config, "url": config["dir"]}
|
||||
|
||||
settings = {
|
||||
Config.PRIVATE_CWD: config.get(
|
||||
Config.PRIVATE_CWD, repo.dvc_dir
|
||||
),
|
||||
Config.SECTION_REMOTE_URL: cache_dir,
|
||||
Config.SECTION_CACHE_TYPE: cache_type,
|
||||
Config.SECTION_CACHE_PROTECTED: protected,
|
||||
Config.SECTION_CACHE_SHARED: shared,
|
||||
}
|
||||
tree = get_cloud_tree(repo, **settings)
|
||||
self.local = LocalCache(tree)
|
||||
|
||||
self.local = Remote(repo, **settings)
|
||||
|
||||
s3 = _make_remote_property(Config.SECTION_CACHE_S3)
|
||||
gs = _make_remote_property(Config.SECTION_CACHE_GS)
|
||||
ssh = _make_remote_property(Config.SECTION_CACHE_SSH)
|
||||
hdfs = _make_remote_property(Config.SECTION_CACHE_HDFS)
|
||||
azure = _make_remote_property(Config.SECTION_CACHE_AZURE)
|
||||
s3 = _make_remote_property("s3")
|
||||
gs = _make_remote_property("gs")
|
||||
ssh = _make_remote_property("ssh")
|
||||
hdfs = _make_remote_property("hdfs")
|
||||
azure = _make_remote_property("azure")
|
||||
|
||||
|
||||
class NamedCache(object):
|
||||
class NamedCacheItem:
|
||||
def __init__(self):
|
||||
self._items = defaultdict(lambda: defaultdict(set))
|
||||
self.names = set()
|
||||
self.children = defaultdict(NamedCacheItem)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.names == other.names and self.children == other.children
|
||||
|
||||
def child_keys(self):
|
||||
for key, child in self.children.items():
|
||||
yield key
|
||||
yield from child.child_keys()
|
||||
|
||||
def child_names(self):
|
||||
for key, child in self.children.items():
|
||||
yield key, child.names
|
||||
yield from child.child_names()
|
||||
|
||||
def add(self, checksum, item):
|
||||
self.children[checksum].update(item)
|
||||
|
||||
def update(self, item, suffix=""):
|
||||
if suffix:
|
||||
self.names.update(n + suffix for n in item.names)
|
||||
else:
|
||||
self.names.update(item.names)
|
||||
for checksum, child_item in item.children.items():
|
||||
self.children[checksum].update(child_item)
|
||||
|
||||
|
||||
class NamedCache:
|
||||
# pylint: disable=protected-access
|
||||
def __init__(self):
|
||||
self._items = defaultdict(lambda: defaultdict(NamedCacheItem))
|
||||
self.external = defaultdict(set)
|
||||
|
||||
@classmethod
|
||||
@@ -118,7 +121,18 @@ class NamedCache(object):
|
||||
return self._items[key]
|
||||
|
||||
def add(self, scheme, checksum, name):
|
||||
self._items[scheme][checksum].add(name)
|
||||
"""Add a mapped name for the specified checksum."""
|
||||
self._items[scheme][checksum].names.add(name)
|
||||
|
||||
def add_child_cache(self, checksum, cache, suffix=""):
|
||||
"""Add/update child cache for the specified checksum."""
|
||||
for scheme, src in cache._items.items():
|
||||
dst = self._items[scheme][checksum].children
|
||||
for child_checksum, item in src.items():
|
||||
dst[child_checksum].update(item, suffix=suffix)
|
||||
|
||||
for repo_pair, files in cache.external.items():
|
||||
self.external[repo_pair].update(files)
|
||||
|
||||
def add_external(self, url, rev, path):
|
||||
self.external[url, rev].add(path)
|
||||
@@ -126,11 +140,32 @@ class NamedCache(object):
|
||||
def update(self, cache, suffix=""):
|
||||
for scheme, src in cache._items.items():
|
||||
dst = self._items[scheme]
|
||||
for checksum, names in src.items():
|
||||
if suffix:
|
||||
dst[checksum].update(n + suffix for n in names)
|
||||
else:
|
||||
dst[checksum].update(names)
|
||||
for checksum, item in src.items():
|
||||
dst[checksum].update(item, suffix=suffix)
|
||||
|
||||
for repo_pair, files in cache.external.items():
|
||||
self.external[repo_pair].update(files)
|
||||
|
||||
def scheme_keys(self, scheme):
|
||||
"""Iterate over a flat list of all keys for the specified scheme,
|
||||
including children.
|
||||
"""
|
||||
for key, item in self._items[scheme].items():
|
||||
yield key
|
||||
yield from item.child_keys()
|
||||
|
||||
def scheme_names(self, scheme):
|
||||
"""Iterate over a flat list of checksum, names items for the specified
|
||||
scheme, including children.
|
||||
"""
|
||||
for key, item in self._items[scheme].items():
|
||||
yield key, item.names
|
||||
yield from item.child_names()
|
||||
|
||||
def dir_keys(self, scheme):
|
||||
return (
|
||||
key for key, item in self._items[scheme].items() if item.children
|
||||
)
|
||||
|
||||
def child_keys(self, scheme, checksum):
|
||||
return self._items[scheme][checksum].child_keys()
|
||||
|
||||
+90
-64
@@ -1,40 +1,45 @@
|
||||
"""Command line interface for dvc."""
|
||||
"""DVC command line interface"""
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import dvc.command.add as add
|
||||
import dvc.command.cache as cache
|
||||
import dvc.command.checkout as checkout
|
||||
import dvc.command.commit as commit
|
||||
import dvc.command.config as config
|
||||
import dvc.command.daemon as daemon
|
||||
import dvc.command.data_sync as data_sync
|
||||
import dvc.command.destroy as destroy
|
||||
import dvc.command.diff as diff
|
||||
import dvc.command.gc as gc
|
||||
import dvc.command.get as get
|
||||
import dvc.command.get_url as get_url
|
||||
import dvc.command.imp as imp
|
||||
import dvc.command.imp_url as imp_url
|
||||
import dvc.command.init as init
|
||||
import dvc.command.install as install
|
||||
import dvc.command.lock as lock
|
||||
import dvc.command.metrics as metrics
|
||||
import dvc.command.move as move
|
||||
import dvc.command.pipeline as pipeline
|
||||
import dvc.command.remote as remote
|
||||
import dvc.command.remove as remove
|
||||
import dvc.command.repro as repro
|
||||
import dvc.command.root as root
|
||||
import dvc.command.run as run
|
||||
import dvc.command.tag as tag
|
||||
import dvc.command.unprotect as unprotect
|
||||
import dvc.command.update as update
|
||||
import dvc.command.version as version
|
||||
from dvc.command.base import fix_subparsers
|
||||
from dvc.exceptions import DvcParserError
|
||||
|
||||
from .command import (
|
||||
add,
|
||||
cache,
|
||||
checkout,
|
||||
commit,
|
||||
completion,
|
||||
config,
|
||||
daemon,
|
||||
dag,
|
||||
data_sync,
|
||||
destroy,
|
||||
diff,
|
||||
freeze,
|
||||
gc,
|
||||
get,
|
||||
get_url,
|
||||
git_hook,
|
||||
imp,
|
||||
imp_url,
|
||||
init,
|
||||
install,
|
||||
ls,
|
||||
metrics,
|
||||
move,
|
||||
params,
|
||||
plots,
|
||||
remote,
|
||||
remove,
|
||||
repro,
|
||||
root,
|
||||
run,
|
||||
unprotect,
|
||||
update,
|
||||
version,
|
||||
)
|
||||
from .command.base import fix_subparsers
|
||||
from .exceptions import DvcParserError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,38 +63,59 @@ COMMANDS = [
|
||||
remote,
|
||||
cache,
|
||||
metrics,
|
||||
params,
|
||||
install,
|
||||
root,
|
||||
lock,
|
||||
pipeline,
|
||||
ls,
|
||||
freeze,
|
||||
dag,
|
||||
daemon,
|
||||
commit,
|
||||
tag,
|
||||
completion,
|
||||
diff,
|
||||
version,
|
||||
update,
|
||||
git_hook,
|
||||
plots,
|
||||
]
|
||||
|
||||
|
||||
def _find_parser(parser, cmd_cls):
|
||||
defaults = parser._defaults # pylint: disable=protected-access
|
||||
if not cmd_cls or cmd_cls == defaults.get("func"):
|
||||
parser.print_help()
|
||||
raise DvcParserError()
|
||||
|
||||
actions = parser._actions # pylint: disable=protected-access
|
||||
for action in actions:
|
||||
if not isinstance(action.choices, dict):
|
||||
# NOTE: we are only interested in subparsers
|
||||
continue
|
||||
for subparser in action.choices.values():
|
||||
_find_parser(subparser, cmd_cls)
|
||||
|
||||
|
||||
class DvcParser(argparse.ArgumentParser):
|
||||
"""Custom parser class for dvc CLI."""
|
||||
|
||||
def error(self, message):
|
||||
"""Custom error method.
|
||||
Args:
|
||||
message (str): error message.
|
||||
|
||||
Raises:
|
||||
dvc.exceptions.DvcParser: dvc parser exception.
|
||||
"""
|
||||
def error(self, message, cmd_cls=None): # pylint: disable=arguments-differ
|
||||
logger.error(message)
|
||||
self.print_help()
|
||||
raise DvcParserError()
|
||||
_find_parser(self, cmd_cls)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
# NOTE: overriding to provide a more granular help message.
|
||||
# E.g. `dvc plots diff --bad-flag` would result in a `dvc plots diff`
|
||||
# help message instead of generic `dvc` usage.
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
if argv:
|
||||
msg = "unrecognized arguments: %s"
|
||||
self.error(msg % " ".join(argv), getattr(args, "func", None))
|
||||
return args
|
||||
|
||||
|
||||
class VersionAction(argparse.Action): # pragma: no cover
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Shows dvc version and exits."""
|
||||
"""Shows DVC version and exits."""
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
from dvc import __version__
|
||||
@@ -109,28 +135,16 @@ def get_parent_parser():
|
||||
|
||||
log_level_group = parent_parser.add_mutually_exclusive_group()
|
||||
log_level_group.add_argument(
|
||||
"-q", "--quiet", action="store_true", default=False, help="Be quiet."
|
||||
"-q", "--quiet", action="count", default=0, help="Be quiet."
|
||||
)
|
||||
log_level_group.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Be verbose.",
|
||||
"-v", "--verbose", action="count", default=0, help="Be verbose."
|
||||
)
|
||||
|
||||
return parent_parser
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
"""Parses CLI arguments.
|
||||
|
||||
Args:
|
||||
argv: optional list of arguments to parse. sys.argv is used by default.
|
||||
|
||||
Raises:
|
||||
dvc.exceptions.DvcParserError: raised for argument parsing errors.
|
||||
"""
|
||||
def get_main_parser():
|
||||
parent_parser = get_parent_parser()
|
||||
|
||||
# Main parser
|
||||
@@ -170,7 +184,7 @@ def parse_args(argv=None):
|
||||
title="Available Commands",
|
||||
metavar="COMMAND",
|
||||
dest="cmd",
|
||||
help="Use dvc COMMAND --help for command-specific help.",
|
||||
help="Use `dvc COMMAND --help` for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(subparsers)
|
||||
@@ -178,6 +192,18 @@ def parse_args(argv=None):
|
||||
for cmd in COMMANDS:
|
||||
cmd.add_parser(subparsers, parent_parser)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
return parser
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
"""Parses CLI arguments.
|
||||
|
||||
Args:
|
||||
argv: optional list of arguments to parse. sys.argv is used by default.
|
||||
|
||||
Raises:
|
||||
dvc.exceptions.DvcParserError: raised for argument parsing errors.
|
||||
"""
|
||||
parser = get_main_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return args
|
||||
|
||||
+14
-9
@@ -1,11 +1,9 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.exceptions import RecursiveAddingWhileUsingFilename
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException, RecursiveAddingWhileUsingFilename
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -21,6 +19,7 @@ class CmdAdd(CmdBase):
|
||||
recursive=self.args.recursive,
|
||||
no_commit=self.args.no_commit,
|
||||
fname=self.args.file,
|
||||
external=self.args.external,
|
||||
)
|
||||
|
||||
except DvcException:
|
||||
@@ -53,11 +52,17 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Don't put files/directories into cache.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
help="Specify name of the DVC-file this command will generate.",
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow targets that are outside of the DVC repository.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add."
|
||||
"--file",
|
||||
help="Specify name of the DVC-file this command will generate.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets", nargs="+", help="Input files/directories to add.",
|
||||
).complete = completion.FILE
|
||||
parser.set_defaults(func=CmdAdd)
|
||||
|
||||
+11
-12
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,7 +27,7 @@ def append_doc_link(help_message, path):
|
||||
)
|
||||
|
||||
|
||||
class CmdBase(object):
|
||||
class CmdBase(ABC):
|
||||
def __init__(self, args):
|
||||
from dvc.repo import Repo
|
||||
from dvc.updater import Updater
|
||||
@@ -35,24 +35,23 @@ class CmdBase(object):
|
||||
self.repo = Repo()
|
||||
self.config = self.repo.config
|
||||
self.args = args
|
||||
hardlink_lock = self.config.config["core"].get("hardlink_lock", False)
|
||||
updater = Updater(self.repo.dvc_dir, hardlink_lock=hardlink_lock)
|
||||
hardlink_lock = self.config["core"].get("hardlink_lock", False)
|
||||
updater = Updater(self.repo.tmp_dir, hardlink_lock=hardlink_lock)
|
||||
updater.check()
|
||||
|
||||
@property
|
||||
def default_targets(self):
|
||||
"""Default targets for `dvc repro` and `dvc pipeline`."""
|
||||
from dvc.stage import Stage
|
||||
"""Default targets for `dvc repro`."""
|
||||
from dvc.dvcfile import PIPELINE_FILE
|
||||
|
||||
msg = "assuming default target '{}'.".format(Stage.STAGE_FILE)
|
||||
logger.warning(msg)
|
||||
return [Stage.STAGE_FILE]
|
||||
logger.debug(f"assuming default target '{PIPELINE_FILE}'.")
|
||||
return [PIPELINE_FILE]
|
||||
|
||||
# Abstract methods that have to be implemented by any inheritance class
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
raise NotImplementedError
|
||||
pass
|
||||
|
||||
|
||||
class CmdBaseNoRepo(CmdBase):
|
||||
def __init__(self, args):
|
||||
def __init__(self, args): # pylint: disable=super-init-not-called
|
||||
self.args = args
|
||||
|
||||
+7
-10
@@ -1,18 +1,14 @@
|
||||
import argparse
|
||||
|
||||
from dvc.cache import CacheConfig
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import fix_subparsers
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import append_doc_link, fix_subparsers
|
||||
from dvc.command.config import CmdConfig
|
||||
|
||||
|
||||
class CmdCacheDir(CmdConfig):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
self.cache_config = CacheConfig(self.config)
|
||||
|
||||
def run(self):
|
||||
self.cache_config.set_dir(self.args.value, level=self.args.level)
|
||||
with self.config.edit(level=self.args.level) as edit:
|
||||
edit["cache"]["dir"] = self.args.value
|
||||
return 0
|
||||
|
||||
|
||||
@@ -30,7 +26,8 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
|
||||
cache_subparsers = cache_parser.add_subparsers(
|
||||
dest="cmd", help="Use dvc cache CMD --help for command-specific help."
|
||||
dest="cmd",
|
||||
help="Use `dvc cache CMD --help` for command-specific " "help.",
|
||||
)
|
||||
|
||||
fix_subparsers(cache_subparsers)
|
||||
@@ -59,5 +56,5 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Path to cache directory. Relative paths are resolved relative "
|
||||
"to the current directory and saved to config relative to the "
|
||||
"config file location.",
|
||||
)
|
||||
).complete = completion.DIR
|
||||
cache_dir_parser.set_defaults(func=CmdCacheDir)
|
||||
|
||||
+70
-12
@@ -1,18 +1,68 @@
|
||||
import argparse
|
||||
import logging
|
||||
import operator
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
import colorama
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import CheckoutError
|
||||
from dvc.utils.humanize import get_summary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def log_changes(stats):
|
||||
colors = [
|
||||
("modified", colorama.Fore.YELLOW,),
|
||||
("added", colorama.Fore.GREEN),
|
||||
("deleted", colorama.Fore.RED,),
|
||||
]
|
||||
|
||||
for state, color in colors:
|
||||
entries = stats.get(state)
|
||||
|
||||
if not entries:
|
||||
continue
|
||||
|
||||
for entry in entries:
|
||||
logger.info(
|
||||
"{color}{state}{nc}{spacing}{entry}".format(
|
||||
color=color,
|
||||
state=state[0].upper(),
|
||||
nc=colorama.Fore.RESET,
|
||||
spacing="\t",
|
||||
entry=entry,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class CmdCheckout(CmdBase):
|
||||
def run(self):
|
||||
self.repo.checkout(
|
||||
targets=self.args.targets,
|
||||
with_deps=self.args.with_deps,
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
recursive=self.args.recursive,
|
||||
)
|
||||
stats, exc = None, None
|
||||
try:
|
||||
stats = self.repo.checkout(
|
||||
targets=self.args.targets,
|
||||
with_deps=self.args.with_deps,
|
||||
force=self.args.force,
|
||||
relink=self.args.relink,
|
||||
recursive=self.args.recursive,
|
||||
)
|
||||
except CheckoutError as _exc:
|
||||
exc = _exc
|
||||
stats = exc.stats
|
||||
|
||||
if self.args.summary:
|
||||
default_message = "No changes."
|
||||
msg = get_summary(
|
||||
sorted(stats.items(), key=operator.itemgetter(0))
|
||||
)
|
||||
logger.info(msg or default_message)
|
||||
else:
|
||||
log_changes(stats)
|
||||
|
||||
if exc:
|
||||
raise exc
|
||||
return 0
|
||||
|
||||
|
||||
@@ -26,6 +76,12 @@ def add_parser(subparsers, parent_parser):
|
||||
help=CHECKOUT_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
checkout_parser.add_argument(
|
||||
"--summary",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show summary of the changes.",
|
||||
)
|
||||
checkout_parser.add_argument(
|
||||
"-d",
|
||||
"--with-deps",
|
||||
@@ -56,7 +112,9 @@ def add_parser(subparsers, parent_parser):
|
||||
checkout_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="DVC-files to checkout. Optional. "
|
||||
"(Finds all DVC-files in the workspace by default.)",
|
||||
)
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVC_FILE
|
||||
checkout_parser.set_defaults(func=CmdCheckout)
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -47,7 +46,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Commit even if checksums for dependencies/outputs changed.",
|
||||
help="Commit even if hash value for dependencies/outputs changed.",
|
||||
)
|
||||
commit_parser.add_argument(
|
||||
"-d",
|
||||
@@ -68,5 +67,5 @@ def add_parser(subparsers, parent_parser):
|
||||
nargs="*",
|
||||
help="DVC-files to commit. Optional. "
|
||||
"(Finds all DVC-files in the workspace by default.)",
|
||||
)
|
||||
).complete = completion.DVC_FILE
|
||||
commit_parser.set_defaults(func=CmdCommit)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import shtab
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
FILE = shtab.FILE
|
||||
DIR = shtab.DIRECTORY
|
||||
DVC_FILE = {
|
||||
"bash": "_dvc_compgen_DVCFiles",
|
||||
"zsh": "_files -g '(*?.dvc|Dvcfile|dvc.yaml)'",
|
||||
}
|
||||
PREAMBLE = {
|
||||
"bash": """
|
||||
# $1=COMP_WORDS[1]
|
||||
_dvc_compgen_DVCFiles() {
|
||||
compgen -d -S '/' -- $1 # recurse into subdirs
|
||||
compgen -f -X '!*?.dvc' -- $1
|
||||
compgen -f -X '!*Dvcfile' -- $1
|
||||
compgen -f -X '!*dvc.yaml' -- $1
|
||||
}
|
||||
""",
|
||||
"zsh": "",
|
||||
}
|
||||
|
||||
|
||||
class CmdCompletion(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.cli import get_main_parser
|
||||
|
||||
parser = get_main_parser()
|
||||
shell = self.args.shell
|
||||
script = shtab.complete(parser, shell=shell, preamble=PREAMBLE)
|
||||
print(script)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
COMPLETION_HELP = "Generate shell tab completion."
|
||||
COMPLETION_DESCRIPTION = "Prints out shell tab completion scripts."
|
||||
completion_parser = subparsers.add_parser(
|
||||
"completion",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(COMPLETION_DESCRIPTION, "completion"),
|
||||
help=COMPLETION_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
completion_parser.add_argument(
|
||||
"-s",
|
||||
"--shell",
|
||||
help="Shell syntax for completions.",
|
||||
default="bash",
|
||||
choices=["bash", "zsh"],
|
||||
)
|
||||
completion_parser.set_defaults(func=CmdCompletion)
|
||||
+33
-21
@@ -1,10 +1,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBaseNoRepo
|
||||
from dvc.config import Config
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.config import Config, ConfigError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,17 +16,21 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
section, opt = self.args.name.lower().strip().split(".", 1)
|
||||
|
||||
if self.args.unset:
|
||||
self.config.unset(section, opt, level=self.args.level)
|
||||
elif self.args.value is None:
|
||||
logger.info(self.config.get(section, opt, level=self.args.level))
|
||||
else:
|
||||
self.config.set(
|
||||
section, opt, self.args.value, level=self.args.level
|
||||
)
|
||||
if self.args.value is None and not self.args.unset:
|
||||
conf = self.config.load_one(self.args.level)
|
||||
self._check(conf, section, opt)
|
||||
logger.info(conf[section][opt])
|
||||
return 0
|
||||
|
||||
is_write = self.args.unset or self.args.value is not None
|
||||
if is_write and self.args.name == "cache.type":
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
self._check(conf, section, opt)
|
||||
del conf[section][opt]
|
||||
else:
|
||||
self._check(conf, section)
|
||||
conf[section][opt] = self.args.value
|
||||
|
||||
if self.args.name == "cache.type":
|
||||
logger.warning(
|
||||
"You have changed the 'cache.type' option. This doesn't update"
|
||||
" any existing workspace file links, but it can be done with:"
|
||||
@@ -37,30 +39,40 @@ class CmdConfig(CmdBaseNoRepo):
|
||||
|
||||
return 0
|
||||
|
||||
def _check(self, conf, section, opt=None):
|
||||
if section not in conf:
|
||||
msg = "section {} doesn't exist"
|
||||
raise ConfigError(msg.format(self.args.name))
|
||||
|
||||
if opt and opt not in conf[section]:
|
||||
msg = "option {} doesn't exist"
|
||||
raise ConfigError(msg.format(self.args.name))
|
||||
|
||||
|
||||
parent_config_parser = argparse.ArgumentParser(add_help=False)
|
||||
parent_config_parser.add_argument(
|
||||
level_group = parent_config_parser.add_mutually_exclusive_group()
|
||||
level_group.add_argument(
|
||||
"--global",
|
||||
dest="level",
|
||||
action="store_const",
|
||||
const=Config.LEVEL_GLOBAL,
|
||||
const="global",
|
||||
help="Use global config.",
|
||||
)
|
||||
parent_config_parser.add_argument(
|
||||
level_group.add_argument(
|
||||
"--system",
|
||||
dest="level",
|
||||
action="store_const",
|
||||
const=Config.LEVEL_SYSTEM,
|
||||
const="system",
|
||||
help="Use system config.",
|
||||
)
|
||||
parent_config_parser.add_argument(
|
||||
level_group.add_argument(
|
||||
"--local",
|
||||
dest="level",
|
||||
action="store_const",
|
||||
const=Config.LEVEL_LOCAL,
|
||||
const="local",
|
||||
help="Use local config.",
|
||||
)
|
||||
parent_config_parser.set_defaults(level=Config.LEVEL_REPO)
|
||||
parent_config_parser.set_defaults(level="repo")
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
|
||||
+10
-8
@@ -1,5 +1,5 @@
|
||||
from dvc.command.base import CmdBaseNoRepo
|
||||
from dvc.command.base import fix_subparsers
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
|
||||
|
||||
|
||||
class CmdDaemonBase(CmdBaseNoRepo):
|
||||
@@ -15,11 +15,10 @@ class CmdDaemonUpdater(CmdDaemonBase):
|
||||
|
||||
root_dir = Repo.find_root()
|
||||
dvc_dir = os.path.join(root_dir, Repo.DVC_DIR)
|
||||
tmp_dir = os.path.join(dvc_dir, "tmp")
|
||||
config = Config(dvc_dir, validate=False)
|
||||
hardlink_lock = config.config.get("core", {}).get(
|
||||
"hardlink_lock", False
|
||||
)
|
||||
updater = Updater(dvc_dir, hardlink_lock=hardlink_lock)
|
||||
hardlink_lock = config.get("core", {}).get("hardlink_lock", False)
|
||||
updater = Updater(tmp_dir, hardlink_lock=hardlink_lock)
|
||||
updater.fetch(detach=False)
|
||||
|
||||
return 0
|
||||
@@ -44,7 +43,8 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
|
||||
daemon_subparsers = daemon_parser.add_subparsers(
|
||||
dest="cmd", help="Use dvc daemon CMD --help for command-specific help."
|
||||
dest="cmd",
|
||||
help="Use `dvc daemon CMD --help` for command-specific " "help.",
|
||||
)
|
||||
|
||||
fix_subparsers(daemon_subparsers)
|
||||
@@ -65,5 +65,7 @@ def add_parser(subparsers, parent_parser):
|
||||
description=DAEMON_ANALYTICS_HELP,
|
||||
help=DAEMON_ANALYTICS_HELP,
|
||||
)
|
||||
daemon_analytics_parser.add_argument("target", help="Analytics file.")
|
||||
daemon_analytics_parser.add_argument(
|
||||
"target", help="Analytics file.",
|
||||
).complete = completion.FILE
|
||||
daemon_analytics_parser.set_defaults(func=CmdDaemonAnalytics)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _show_ascii(G):
|
||||
from dvc.repo.graph import get_pipelines
|
||||
from dvc.dagascii import draw
|
||||
|
||||
pipelines = get_pipelines(G)
|
||||
|
||||
ret = []
|
||||
for pipeline in pipelines:
|
||||
ret.append(draw(pipeline.nodes, pipeline.edges))
|
||||
|
||||
return "\n".join(ret)
|
||||
|
||||
|
||||
def _show_dot(G):
|
||||
import io
|
||||
from networkx.drawing.nx_pydot import write_dot
|
||||
|
||||
dot_file = io.StringIO()
|
||||
write_dot(G, dot_file)
|
||||
return dot_file.getvalue()
|
||||
|
||||
|
||||
def _build(G, target=None, full=False):
|
||||
import networkx as nx
|
||||
from dvc.repo.graph import get_pipeline, get_pipelines
|
||||
|
||||
if target:
|
||||
H = get_pipeline(get_pipelines(G), target)
|
||||
if not full:
|
||||
descendants = nx.descendants(G, target)
|
||||
descendants.add(target)
|
||||
H.remove_nodes_from(set(G.nodes()) - descendants)
|
||||
else:
|
||||
H = G
|
||||
|
||||
def _relabel(stage):
|
||||
return stage.addressing
|
||||
|
||||
return nx.relabel_nodes(H, _relabel, copy=False)
|
||||
|
||||
|
||||
class CmdDAG(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
target = None
|
||||
if self.args.target:
|
||||
stages = self.repo.collect(self.args.target)
|
||||
if len(stages) > 1:
|
||||
logger.error(
|
||||
f"'{self.args.target}' contains more than one stage "
|
||||
"{stages}, please specify one stage"
|
||||
)
|
||||
return 1
|
||||
target = stages[0]
|
||||
|
||||
G = _build(self.repo.graph, target=target, full=self.args.full,)
|
||||
|
||||
if self.args.dot:
|
||||
logger.info(_show_dot(G))
|
||||
else:
|
||||
from dvc.utils.pager import pager
|
||||
|
||||
pager(_show_ascii(G))
|
||||
|
||||
return 0
|
||||
except DvcException:
|
||||
msg = "failed to show "
|
||||
if self.args.target:
|
||||
msg += f"a pipeline for '{target}'"
|
||||
else:
|
||||
msg += "pipelines"
|
||||
logger.exception(msg)
|
||||
return 1
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
DAG_HELP = "Visualize DVC project DAG."
|
||||
dag_parser = subparsers.add_parser(
|
||||
"dag",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DAG_HELP, "dag"),
|
||||
help=DAG_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
dag_parser.add_argument(
|
||||
"--dot",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print DAG with .dot format.",
|
||||
)
|
||||
dag_parser.add_argument(
|
||||
"--full",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Show full DAG that the target belongs too, instead of "
|
||||
"showing DAG consisting only of ancestors."
|
||||
),
|
||||
)
|
||||
dag_parser.add_argument(
|
||||
"target",
|
||||
nargs="?",
|
||||
help="Stage or output to show pipeline for (optional). "
|
||||
"Finds all stages in the workspace by default.",
|
||||
)
|
||||
dag_parser.set_defaults(func=CmdDAG)
|
||||
+113
-29
@@ -1,38 +1,46 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.command.checkout import log_changes
|
||||
from dvc.exceptions import CheckoutError, DvcException
|
||||
from dvc.utils.humanize import get_summary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdDataBase(CmdBase):
|
||||
@classmethod
|
||||
def check_up_to_date(cls, processed_files_count):
|
||||
if processed_files_count == 0:
|
||||
logger.info("Everything is up to date.")
|
||||
def log_summary(self, stats):
|
||||
default_msg = "Everything is up to date."
|
||||
logger.info(get_summary(stats.items()) or default_msg)
|
||||
|
||||
|
||||
class CmdDataPull(CmdDataBase):
|
||||
def log_summary(self, stats):
|
||||
log_changes(stats)
|
||||
super().log_summary(stats)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
processed_files_count = self.repo.pull(
|
||||
stats = self.repo.pull(
|
||||
targets=self.args.targets,
|
||||
jobs=self.args.jobs,
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
with_deps=self.args.with_deps,
|
||||
force=self.args.force,
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
except DvcException:
|
||||
self.log_summary(stats)
|
||||
except (CheckoutError, DvcException) as exc:
|
||||
self.log_summary(getattr(exc, "stats", {}))
|
||||
logger.exception("failed to pull data from the cloud")
|
||||
return 1
|
||||
self.check_up_to_date(processed_files_count)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -45,13 +53,15 @@ class CmdDataPush(CmdDataBase):
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
self.log_summary({"pushed": processed_files_count})
|
||||
except DvcException:
|
||||
logger.exception("failed to push data to the cloud")
|
||||
return 1
|
||||
self.check_up_to_date(processed_files_count)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -64,13 +74,15 @@ class CmdDataFetch(CmdDataBase):
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
run_cache=self.args.run_cache,
|
||||
)
|
||||
self.log_summary({"fetched": processed_files_count})
|
||||
except DvcException:
|
||||
logger.exception("failed to fetch data from the cloud")
|
||||
return 1
|
||||
self.check_up_to_date(processed_files_count)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -78,27 +90,33 @@ def shared_parent_parser():
|
||||
from dvc.cli import get_parent_parser
|
||||
|
||||
# Parent parser used in pull/push/status
|
||||
shared_parent_parser = argparse.ArgumentParser(
|
||||
parent_parser = argparse.ArgumentParser(
|
||||
add_help=False, parents=[get_parent_parser()]
|
||||
)
|
||||
shared_parent_parser.add_argument(
|
||||
"-j", "--jobs", type=int, help="Number of jobs to run simultaneously."
|
||||
parent_parser.add_argument(
|
||||
"-j",
|
||||
"--jobs",
|
||||
type=int,
|
||||
help="Number of jobs to run simultaneously.",
|
||||
metavar="<number>",
|
||||
)
|
||||
shared_parent_parser.add_argument(
|
||||
parent_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Limit command scope to these DVC-files. "
|
||||
"Using -R, directories to search DVC-files in can also be given.",
|
||||
)
|
||||
help=(
|
||||
"Limit command scope to these tracked files/directories, "
|
||||
".dvc files, or stage names."
|
||||
),
|
||||
).complete = completion.DVC_FILE
|
||||
|
||||
return shared_parent_parser
|
||||
return parent_parser
|
||||
|
||||
|
||||
def add_parser(subparsers, _parent_parser):
|
||||
from dvc.command.status import CmdDataStatus
|
||||
|
||||
# Pull
|
||||
PULL_HELP = "Pull data files from a DVC remote storage."
|
||||
PULL_HELP = "Download tracked files or directories from remote storage."
|
||||
|
||||
pull_parser = subparsers.add_parser(
|
||||
"pull",
|
||||
@@ -108,7 +126,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-r", "--remote", help="Remote repository to pull from."
|
||||
"-r", "--remote", help="Remote storage to pull from", metavar="<name>",
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-a",
|
||||
@@ -124,6 +142,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Fetch cache for all tags.",
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Fetch cache for all commits.",
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
@@ -145,10 +169,16 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Pull cache for subdirectories of the specified directory.",
|
||||
)
|
||||
pull_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
pull_parser.set_defaults(func=CmdDataPull)
|
||||
|
||||
# Push
|
||||
PUSH_HELP = "Push data files to a DVC remote storage."
|
||||
PUSH_HELP = "Upload tracked files or directories to remote storage."
|
||||
|
||||
push_parser = subparsers.add_parser(
|
||||
"push",
|
||||
@@ -158,7 +188,7 @@ def add_parser(subparsers, _parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-r", "--remote", help="Remote repository to push to."
|
||||
"-r", "--remote", help="Remote storage to push to", metavar="<name>",
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-a",
|
||||
@@ -174,6 +204,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Push cache for all tags.",
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Push cache for all commits.",
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"-d",
|
||||
"--with-deps",
|
||||
@@ -186,12 +222,21 @@ def add_parser(subparsers, _parent_parser):
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Push cache from subdirectories of specified directory.",
|
||||
help="Push cache for subdirectories of specified directory.",
|
||||
)
|
||||
push_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Push run history for all stages.",
|
||||
)
|
||||
push_parser.set_defaults(func=CmdDataPush)
|
||||
|
||||
# Fetch
|
||||
FETCH_HELP = "Fetch data files from a DVC remote storage."
|
||||
FETCH_HELP = (
|
||||
"Get tracked files or directories from remote storage"
|
||||
" into the cache."
|
||||
)
|
||||
|
||||
fetch_parser = subparsers.add_parser(
|
||||
"fetch",
|
||||
@@ -201,7 +246,10 @@ def add_parser(subparsers, _parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-r", "--remote", help="Remote repository to fetch from."
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to fetch from",
|
||||
metavar="<name>",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-a",
|
||||
@@ -217,6 +265,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Fetch cache for all tags.",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Fetch cache for all commits.",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"-d",
|
||||
"--with-deps",
|
||||
@@ -231,6 +285,12 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Fetch cache for subdirectories of specified directory.",
|
||||
)
|
||||
fetch_parser.add_argument(
|
||||
"--run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Fetch run history for all stages.",
|
||||
)
|
||||
fetch_parser.set_defaults(func=CmdDataFetch)
|
||||
|
||||
# Status
|
||||
@@ -264,7 +324,10 @@ def add_parser(subparsers, _parent_parser):
|
||||
help="Show status of a local cache compared to a remote repository.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"-r", "--remote", help="Remote repository to compare local cache to."
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to compare local cache to",
|
||||
metavar="<name>",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"-a",
|
||||
@@ -282,6 +345,13 @@ def add_parser(subparsers, _parent_parser):
|
||||
help="Show status of a local cache compared to a remote repository "
|
||||
"for all tags.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show status of a local cache compared to a remote repository "
|
||||
"for all commits.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"-d",
|
||||
"--with-deps",
|
||||
@@ -289,4 +359,18 @@ def add_parser(subparsers, _parent_parser):
|
||||
default=False,
|
||||
help="Show status for all dependencies of the specified target.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show status of all stages in the specified directory.",
|
||||
)
|
||||
status_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show status in JSON format.",
|
||||
)
|
||||
|
||||
status_parser.set_defaults(func=CmdDataStatus)
|
||||
|
||||
@@ -2,11 +2,9 @@ import argparse
|
||||
import logging
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -27,7 +25,7 @@ class CmdDestroy(CmdBase):
|
||||
)
|
||||
|
||||
self.repo.destroy()
|
||||
except Exception:
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
logger.exception("failed to destroy DVC")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
+134
-131
@@ -1,170 +1,173 @@
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import humanize
|
||||
import inflect
|
||||
from funcy import compact
|
||||
import colorama
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _show_md(diff):
|
||||
from dvc.utils.diff import table
|
||||
|
||||
rows = []
|
||||
for status in ["added", "deleted", "modified"]:
|
||||
entries = diff.get(status, [])
|
||||
if not entries:
|
||||
continue
|
||||
paths = sorted(entry["path"] for entry in entries)
|
||||
for path in paths:
|
||||
rows.append([status, path])
|
||||
|
||||
return table(["Status", "Path"], rows, True)
|
||||
|
||||
|
||||
class CmdDiff(CmdBase):
|
||||
@staticmethod
|
||||
def _print_size(size):
|
||||
if size < 0:
|
||||
change = "decreased by {}"
|
||||
elif size > 0:
|
||||
change = "increased by {}"
|
||||
else:
|
||||
change = "not changed"
|
||||
natur_size = humanize.naturalsize(abs(size))
|
||||
return change.format(natur_size)
|
||||
def _format(diff):
|
||||
"""
|
||||
Given a diff structure, generate a string of paths separated
|
||||
by new lines and grouped together by their state.
|
||||
|
||||
@staticmethod
|
||||
def _get_md5_string(sign, file_name, checksum):
|
||||
sample_msg = ""
|
||||
if file_name:
|
||||
sample_msg = "{}{} with md5 {}\n"
|
||||
sample_msg = sample_msg.format(sign, file_name, checksum)
|
||||
return sample_msg
|
||||
A group's header is colored and its entries are sorted to enhance
|
||||
readability, for example:
|
||||
|
||||
@classmethod
|
||||
def _get_dir_changes(cls, dct):
|
||||
import dvc.repo.diff as diff
|
||||
Added:
|
||||
another_file.txt
|
||||
backup.tar
|
||||
dir/
|
||||
dir/1
|
||||
|
||||
engine = inflect.engine()
|
||||
changes_msg = (
|
||||
"{} {} untouched, {} {} modified, {} {} added, "
|
||||
"{} {} deleted, size was {}"
|
||||
An example of a diff formatted when entries contain hash:
|
||||
|
||||
Added:
|
||||
d3b07384 foo
|
||||
|
||||
Modified:
|
||||
c157a790..f98bf6f1 bar
|
||||
|
||||
If a group has no entries, it won't be included in the result.
|
||||
|
||||
At the bottom, include a summary with the number of files per state.
|
||||
"""
|
||||
|
||||
def _digest(checksum):
|
||||
if isinstance(checksum, str):
|
||||
return checksum[0:8]
|
||||
return "{}..{}".format(checksum["old"][0:8], checksum["new"][0:8])
|
||||
|
||||
colors = {
|
||||
"added": colorama.Fore.GREEN,
|
||||
"modified": colorama.Fore.YELLOW,
|
||||
"deleted": colorama.Fore.RED,
|
||||
}
|
||||
|
||||
summary = {}
|
||||
groups = []
|
||||
|
||||
for state in ["added", "deleted", "modified"]:
|
||||
summary[state] = 0
|
||||
entries = diff[state]
|
||||
|
||||
if not entries:
|
||||
continue
|
||||
|
||||
content = []
|
||||
|
||||
for entry in entries:
|
||||
path = entry["path"]
|
||||
checksum = entry.get("hash")
|
||||
summary[state] += 1 if not path.endswith(os.sep) else 0
|
||||
content.append(
|
||||
"{space}{checksum}{separator}{path}".format(
|
||||
space=" ",
|
||||
checksum=_digest(checksum) if checksum else "",
|
||||
separator=" " if checksum else "",
|
||||
path=entry["path"],
|
||||
)
|
||||
)
|
||||
|
||||
groups.append(
|
||||
"{color}{header}{nc}:\n{content}".format(
|
||||
color=colors[state],
|
||||
header=state.capitalize(),
|
||||
nc=colorama.Fore.RESET,
|
||||
content="\n".join(content),
|
||||
)
|
||||
)
|
||||
|
||||
groups.append(
|
||||
"files summary: {added} added, {deleted} deleted,"
|
||||
" {modified} modified".format_map(summary)
|
||||
)
|
||||
changes_msg = changes_msg.format(
|
||||
dct[diff.DIFF_IDENT],
|
||||
engine.plural("file", dct[diff.DIFF_IDENT]),
|
||||
dct[diff.DIFF_CHANGE],
|
||||
engine.plural("file", dct[diff.DIFF_CHANGE]),
|
||||
dct[diff.DIFF_NEW],
|
||||
engine.plural("file", dct[diff.DIFF_NEW]),
|
||||
dct[diff.DIFF_DEL],
|
||||
engine.plural("file", dct[diff.DIFF_DEL]),
|
||||
cls._print_size(dct[diff.DIFF_SIZE]),
|
||||
)
|
||||
return changes_msg
|
||||
|
||||
@classmethod
|
||||
def _get_file_changes(cls, dct):
|
||||
import dvc.repo.diff as diff
|
||||
|
||||
if (
|
||||
dct.get(diff.DIFF_OLD_FILE)
|
||||
and dct.get(diff.DIFF_NEW_FILE)
|
||||
and dct[diff.DIFF_SIZE] == 0
|
||||
):
|
||||
msg = "file size was not changed"
|
||||
elif dct.get(diff.DIFF_NEW_FILE):
|
||||
msg = "added file with size {}".format(
|
||||
humanize.naturalsize(dct[diff.DIFF_SIZE])
|
||||
)
|
||||
elif dct.get(diff.DIFF_OLD_FILE):
|
||||
msg = "deleted file with size {}".format(
|
||||
humanize.naturalsize(abs(dct[diff.DIFF_SIZE]))
|
||||
)
|
||||
else:
|
||||
msg = "file was modified, file size {}".format(
|
||||
cls._print_size(dct[diff.DIFF_SIZE])
|
||||
)
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def _get_royal_changes(cls, dct):
|
||||
import dvc.repo.diff as diff
|
||||
|
||||
if dct[diff.DIFF_SIZE] != diff.DIFF_SIZE_UNKNOWN:
|
||||
if dct.get("is_dir"):
|
||||
return cls._get_dir_changes(dct)
|
||||
else:
|
||||
return cls._get_file_changes(dct)
|
||||
return "size is ?"
|
||||
|
||||
@classmethod
|
||||
def _show(cls, diff_dct):
|
||||
import dvc.repo.diff as diff
|
||||
|
||||
msg = "dvc diff from {} to {}".format(
|
||||
diff_dct[diff.DIFF_A_REF], diff_dct[diff.DIFF_B_REF]
|
||||
)
|
||||
if diff_dct.get(diff.DIFF_EQUAL):
|
||||
logger.info(msg)
|
||||
return
|
||||
for dct in diff_dct[diff.DIFF_LIST]:
|
||||
msg += "\n\ndiff for '{}'\n".format(dct[diff.DIFF_TARGET])
|
||||
msg += cls._get_md5_string(
|
||||
"-",
|
||||
dct.get(diff.DIFF_OLD_FILE),
|
||||
dct.get(diff.DIFF_OLD_CHECKSUM),
|
||||
)
|
||||
msg += cls._get_md5_string(
|
||||
"+",
|
||||
dct.get(diff.DIFF_NEW_FILE),
|
||||
dct.get(diff.DIFF_NEW_CHECKSUM),
|
||||
)
|
||||
msg += "\n"
|
||||
msg += cls._get_royal_changes(dct)
|
||||
logger.info(msg)
|
||||
return msg
|
||||
return "\n\n".join(groups)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
msg = self.repo.diff(
|
||||
self.args.a_ref, target=self.args.target, b_ref=self.args.b_ref
|
||||
)
|
||||
self._show(msg)
|
||||
diff = self.repo.diff(self.args.a_rev, self.args.b_rev)
|
||||
|
||||
if not self.args.show_hash:
|
||||
for _, entries in diff.items():
|
||||
for entry in entries:
|
||||
del entry["hash"]
|
||||
|
||||
if self.args.show_json:
|
||||
logger.info(json.dumps(diff))
|
||||
elif self.args.show_md:
|
||||
logger.info(_show_md(diff))
|
||||
elif diff:
|
||||
logger.info(self._format(diff))
|
||||
|
||||
except DvcException:
|
||||
msg = "failed to get 'diff {}'"
|
||||
args = " ".join(
|
||||
compact([self.args.target, self.args.a_ref, self.args.b_ref])
|
||||
)
|
||||
msg = msg.format(args)
|
||||
logger.exception(msg)
|
||||
logger.exception("failed to get diff")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
DIFF_DESCRIPTION = (
|
||||
"Show diff of a data file or a directory that is under DVC control.\n"
|
||||
"Some basic statistics summary, how many files were deleted/changed."
|
||||
"Show added, modified, or deleted data between commits in the DVC"
|
||||
" repository, or between a commit and the workspace."
|
||||
)
|
||||
DIFF_HELP = "Show a diff of a DVC controlled data file or a directory."
|
||||
diff_parser = subparsers.add_parser(
|
||||
"diff",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(DIFF_DESCRIPTION, "diff"),
|
||||
help=DIFF_HELP,
|
||||
help=DIFF_DESCRIPTION,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"-t",
|
||||
"--target",
|
||||
help=(
|
||||
"Source path to a data file or directory. Default None. "
|
||||
"If not specified, compares all files and directories "
|
||||
"that are under DVC control in the current working space."
|
||||
),
|
||||
"a_rev",
|
||||
help="Old Git commit to compare (defaults to HEAD)",
|
||||
nargs="?",
|
||||
default="HEAD",
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"a_ref", help="Git reference from which diff calculates"
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"b_ref",
|
||||
help=(
|
||||
"Git reference until which diff calculates, if omitted "
|
||||
"diff shows the difference between current HEAD and a_ref"
|
||||
),
|
||||
"b_rev",
|
||||
help=("New Git commit to compare (defaults to the current workspace)"),
|
||||
nargs="?",
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--show-json",
|
||||
help="Format the output into a JSON",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--show-hash",
|
||||
help="Display hash value for each entry",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
diff_parser.add_argument(
|
||||
"--show-md",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
action="store_true",
|
||||
default=False,
|
||||
)
|
||||
diff_parser.set_defaults(func=CmdDiff)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdFreezeBase(CmdBase):
|
||||
def _run(self, func, name):
|
||||
ret = 0
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
func(target)
|
||||
except DvcException:
|
||||
logger.exception(f"failed to {name} '{target}'")
|
||||
ret = 1
|
||||
return ret
|
||||
|
||||
|
||||
class CmdFreeze(CmdFreezeBase):
|
||||
def run(self):
|
||||
return self._run(self.repo.freeze, "freeze")
|
||||
|
||||
|
||||
class CmdUnfreeze(CmdFreezeBase):
|
||||
def run(self):
|
||||
return self._run(self.repo.unfreeze, "unfreeze")
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
FREEZE_HELP = "Freeze stages or .dvc files."
|
||||
freeze_parser = subparsers.add_parser(
|
||||
"freeze",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(FREEZE_HELP, "freeze"),
|
||||
help=FREEZE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
freeze_parser.add_argument(
|
||||
"targets", nargs="+", help="Stages or .dvc files to freeze",
|
||||
).complete = completion.DVC_FILE
|
||||
freeze_parser.set_defaults(func=CmdFreeze)
|
||||
|
||||
UNFREEZE_HELP = "Unfreeze stages or .dvc files."
|
||||
unfreeze_parser = subparsers.add_parser(
|
||||
"unfreeze",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(UNFREEZE_HELP, "unfreeze"),
|
||||
help=UNFREEZE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
unfreeze_parser.add_argument(
|
||||
"targets", nargs="+", help="Stages or .dvc files to unfreeze",
|
||||
).complete = completion.DVC_FILE
|
||||
unfreeze_parser.set_defaults(func=CmdUnfreeze)
|
||||
+35
-12
@@ -3,18 +3,25 @@ import logging
|
||||
import os
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdGC(CmdBase):
|
||||
def run(self):
|
||||
from dvc.repo.gc import _raise_error_if_all_disabled
|
||||
|
||||
_raise_error_if_all_disabled(
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
workspace=self.args.workspace,
|
||||
)
|
||||
|
||||
msg = "This will remove all cache except items used in "
|
||||
|
||||
msg += "the working tree"
|
||||
msg += "the workspace"
|
||||
if self.args.all_commits:
|
||||
msg += " and all git commits"
|
||||
elif self.args.all_branches and self.args.all_tags:
|
||||
@@ -47,15 +54,16 @@ class CmdGC(CmdBase):
|
||||
force=self.args.force,
|
||||
jobs=self.args.jobs,
|
||||
repos=self.args.repos,
|
||||
workspace=self.args.workspace,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
GC_HELP = "Collect unused data from DVC cache or a remote storage."
|
||||
GC_HELP = "Garbage collect unused objects from cache or 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)."
|
||||
"Removes all files in the cache or a remote which are not in\n"
|
||||
"use by the specified Git revisions (defaults to just HEAD)."
|
||||
)
|
||||
gc_parser = subparsers.add_parser(
|
||||
"gc",
|
||||
@@ -64,25 +72,32 @@ def add_parser(subparsers, parent_parser):
|
||||
help=GC_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-w",
|
||||
"--workspace",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files used in the current workspace.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files for the tips of all git branches.",
|
||||
help="Keep data files for the tips of all Git branches.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-T",
|
||||
"--all-tags",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Keep data files for all git tags.",
|
||||
help="Keep data files for all Git tags.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
help="Keep data files for all Git commits.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-c",
|
||||
@@ -92,7 +107,10 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Collect garbage in remote repository.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-r", "--remote", help="Remote storage to collect garbage in."
|
||||
"-r",
|
||||
"--remote",
|
||||
help="Remote storage to collect garbage in",
|
||||
metavar="<name>",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-f",
|
||||
@@ -102,7 +120,11 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Force garbage collection - automatically agree to all prompts.",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-j", "--jobs", type=int, help="Number of jobs to run simultaneously."
|
||||
"-j",
|
||||
"--jobs",
|
||||
type=int,
|
||||
help="Number of jobs to run simultaneously.",
|
||||
metavar="<number>",
|
||||
)
|
||||
gc_parser.add_argument(
|
||||
"-p",
|
||||
@@ -113,5 +135,6 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Keep data files required by these projects "
|
||||
"in addition to the current one. "
|
||||
"Useful if you share a single cache across repos.",
|
||||
metavar="<paths>",
|
||||
)
|
||||
gc_parser.set_defaults(func=CmdGC)
|
||||
|
||||
+42
-13
@@ -1,16 +1,36 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from .base import append_doc_link
|
||||
from .base import CmdBaseNoRepo
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from . import completion
|
||||
from .base import CmdBaseNoRepo, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdGet(CmdBaseNoRepo):
|
||||
def _show_url(self):
|
||||
from dvc.api import get_url
|
||||
|
||||
try:
|
||||
url = get_url(
|
||||
self.args.path, repo=self.args.url, rev=self.args.rev
|
||||
)
|
||||
logger.info(url)
|
||||
except DvcException:
|
||||
logger.exception("failed to show URL")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
def run(self):
|
||||
if self.args.show_url:
|
||||
return self._show_url()
|
||||
|
||||
return self._get_file_from_repo()
|
||||
|
||||
def _get_file_from_repo(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
@@ -31,9 +51,7 @@ class CmdGet(CmdBaseNoRepo):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
GET_HELP = (
|
||||
"Download a file or directory from any DVC project or Git repository."
|
||||
)
|
||||
GET_HELP = "Download file or directory tracked by DVC or by Git."
|
||||
get_parser = subparsers.add_parser(
|
||||
"get",
|
||||
parents=[parent_parser],
|
||||
@@ -42,17 +60,28 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"url",
|
||||
help="Location of DVC project or Git repository to download from",
|
||||
"url", help="Location of DVC or Git repository to download from"
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"path",
|
||||
help="Path to a file or directory within the project or repository",
|
||||
"path", help="Path to a file or directory within the repository",
|
||||
).complete = completion.FILE
|
||||
get_parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
nargs="?",
|
||||
help="Destination path to download files to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
get_parser.add_argument(
|
||||
"--rev",
|
||||
nargs="?",
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"-o", "--out", nargs="?", help="Destination path to download files to"
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"--rev", nargs="?", help="Git revision (e.g. branch, tag, SHA)"
|
||||
"--show-url",
|
||||
action="store_true",
|
||||
help="Print the storage location (URL) the target data would be "
|
||||
"downloaded from, and exit.",
|
||||
)
|
||||
get_parser.set_defaults(func=CmdGet)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from .base import append_doc_link
|
||||
from .base import CmdBaseNoRepo
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from . import completion
|
||||
from .base import CmdBaseNoRepo, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,7 +17,7 @@ class CmdGetUrl(CmdBaseNoRepo):
|
||||
Repo.get_url(self.args.url, out=self.args.out)
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception("failed to get '{}'".format(self.args.url))
|
||||
logger.exception(f"failed to get '{self.args.url}'")
|
||||
return 1
|
||||
|
||||
|
||||
@@ -34,6 +34,6 @@ def add_parser(subparsers, parent_parser):
|
||||
"url", help="See `dvc import-url -h` for full list of supported URLs."
|
||||
)
|
||||
get_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put data to."
|
||||
)
|
||||
"out", nargs="?", help="Destination path to put data to.",
|
||||
).complete = completion.DIR
|
||||
get_parser.set_defaults(func=CmdGetUrl)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, fix_subparsers
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdHookBase(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
repo = Repo()
|
||||
repo.close()
|
||||
except NotDvcRepoError:
|
||||
return 0
|
||||
|
||||
return self._run()
|
||||
|
||||
def _run(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class CmdPreCommit(CmdHookBase):
|
||||
def _run(self):
|
||||
from dvc.main import main
|
||||
|
||||
return main(["status"])
|
||||
|
||||
|
||||
class CmdPostCheckout(CmdHookBase):
|
||||
def _run(self):
|
||||
# when we are running from pre-commit tool, it doesn't provide CLI
|
||||
# flags, but instead provides respective env vars that we could use.
|
||||
flag = os.environ.get("PRE_COMMIT_CHECKOUT_TYPE")
|
||||
if flag is None and len(self.args.args) >= 3:
|
||||
# see https://git-scm.com/docs/githooks#_post_checkout
|
||||
flag = self.args.args[2]
|
||||
|
||||
# checking out some reference and not specific file.
|
||||
if flag != "1":
|
||||
return 0
|
||||
|
||||
# make sure we are not in the middle of a rebase/merge, so we
|
||||
# don't accidentally break it with an unsuccessful checkout.
|
||||
# Note that git hooks are always running in repo root.
|
||||
if os.path.isdir(os.path.join(".git", "rebase-merge")):
|
||||
return 0
|
||||
|
||||
from dvc.main import main
|
||||
|
||||
return main(["checkout"])
|
||||
|
||||
|
||||
class CmdPrePush(CmdHookBase):
|
||||
def _run(self):
|
||||
from dvc.main import main
|
||||
|
||||
return main(["push"])
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
GIT_HOOK_HELP = "Run GIT hook."
|
||||
|
||||
git_hook_parser = subparsers.add_parser(
|
||||
"git-hook",
|
||||
parents=[parent_parser],
|
||||
description=GIT_HOOK_HELP,
|
||||
add_help=False,
|
||||
)
|
||||
|
||||
git_hook_subparsers = git_hook_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc daemon CMD --help` for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(git_hook_subparsers)
|
||||
|
||||
PRE_COMMIT_HELP = "Run pre-commit GIT hook."
|
||||
pre_commit_parser = git_hook_subparsers.add_parser(
|
||||
"pre-commit",
|
||||
parents=[parent_parser],
|
||||
description=PRE_COMMIT_HELP,
|
||||
help=PRE_COMMIT_HELP,
|
||||
)
|
||||
pre_commit_parser.add_argument(
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
|
||||
)
|
||||
pre_commit_parser.set_defaults(func=CmdPreCommit)
|
||||
|
||||
POST_CHECKOUT_HELP = "Run post-checkout GIT hook."
|
||||
post_checkout_parser = git_hook_subparsers.add_parser(
|
||||
"post-checkout",
|
||||
parents=[parent_parser],
|
||||
description=POST_CHECKOUT_HELP,
|
||||
help=POST_CHECKOUT_HELP,
|
||||
)
|
||||
post_checkout_parser.add_argument(
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
|
||||
)
|
||||
post_checkout_parser.set_defaults(func=CmdPostCheckout)
|
||||
|
||||
PRE_PUSH_HELP = "Run pre-push GIT hook."
|
||||
pre_push_parser = git_hook_subparsers.add_parser(
|
||||
"pre-push",
|
||||
parents=[parent_parser],
|
||||
description=PRE_PUSH_HELP,
|
||||
help=PRE_PUSH_HELP,
|
||||
)
|
||||
pre_push_parser.add_argument(
|
||||
"args", nargs="*", help="Arguments passed by GIT or pre-commit tool.",
|
||||
)
|
||||
pre_push_parser.set_defaults(func=CmdPrePush)
|
||||
+17
-13
@@ -1,11 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -30,8 +29,8 @@ class CmdImport(CmdBase):
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
IMPORT_HELP = (
|
||||
"Download a file or directory from any DVC project or Git repository "
|
||||
"and take it under DVC control."
|
||||
"Download file or directory tracked by DVC or by Git "
|
||||
"into the workspace, and track it."
|
||||
)
|
||||
|
||||
import_parser = subparsers.add_parser(
|
||||
@@ -42,17 +41,22 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"url",
|
||||
help="Location of DVC project or Git repository to download from",
|
||||
"url", help="Location of DVC or Git repository to download from"
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"path",
|
||||
help="Path to a file or directory within the project or repository",
|
||||
)
|
||||
"path", help="Path to a file or directory within the repository",
|
||||
).complete = completion.FILE
|
||||
import_parser.add_argument(
|
||||
"-o", "--out", nargs="?", help="Destination path to download files to"
|
||||
)
|
||||
"-o",
|
||||
"--out",
|
||||
nargs="?",
|
||||
help="Destination path to download files to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--rev", nargs="?", help="Git revision (e.g. branch, tag, SHA)"
|
||||
"--rev",
|
||||
nargs="?",
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
import_parser.set_defaults(func=CmdImport)
|
||||
|
||||
+17
-7
@@ -1,11 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -13,7 +12,10 @@ class CmdImportUrl(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
self.repo.imp_url(
|
||||
self.args.url, out=self.args.out, fname=self.args.file
|
||||
self.args.url,
|
||||
out=self.args.out,
|
||||
fname=self.args.file,
|
||||
no_exec=self.args.no_exec,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
@@ -56,9 +58,17 @@ def add_parser(subparsers, parent_parser):
|
||||
"remote://myremote/path/to/file (see `dvc remote`)",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"out", nargs="?", help="Destination path to put files to."
|
||||
)
|
||||
"out", nargs="?", help="Destination path to put files to.",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"-f", "--file", help="Specify name of the DVC-file it generates."
|
||||
"--file",
|
||||
help="Specify name of the DVC-file this command will generate.",
|
||||
metavar="<filename>",
|
||||
).complete = completion.DIR
|
||||
import_parser.add_argument(
|
||||
"--no-exec",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Only create DVC-file without actually downloading it.",
|
||||
)
|
||||
import_parser.set_defaults(func=CmdImportUrl)
|
||||
|
||||
+17
-7
@@ -1,9 +1,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBaseNoRepo
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -15,11 +13,14 @@ class CmdInit(CmdBaseNoRepo):
|
||||
|
||||
try:
|
||||
self.repo = Repo.init(
|
||||
".", no_scm=self.args.no_scm, force=self.args.force
|
||||
".",
|
||||
no_scm=self.args.no_scm,
|
||||
force=self.args.force,
|
||||
subdir=self.args.subdir,
|
||||
)
|
||||
self.config = self.repo.config
|
||||
except InitError:
|
||||
logger.exception("failed to initiate dvc")
|
||||
logger.exception("failed to initiate DVC")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -43,8 +44,8 @@ def add_parser(subparsers, parent_parser):
|
||||
"--no-scm",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Initiate dvc in directory that is "
|
||||
"not tracked by any scm tool (e.g. git).",
|
||||
help="Initiate DVC in directory that is "
|
||||
"not tracked by any SCM tool (e.g. Git).",
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"-f",
|
||||
@@ -56,4 +57,13 @@ def add_parser(subparsers, parent_parser):
|
||||
"This operation removes local cache."
|
||||
),
|
||||
)
|
||||
init_parser.add_argument(
|
||||
"--subdir",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Necessary for running this command inside a subdirectory of a "
|
||||
"parent SCM repository."
|
||||
),
|
||||
)
|
||||
init_parser.set_defaults(func=CmdInit)
|
||||
|
||||
+11
-6
@@ -1,9 +1,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -11,9 +9,9 @@ logger = logging.getLogger(__name__)
|
||||
class CmdInstall(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
self.repo.install()
|
||||
except Exception:
|
||||
logger.exception("failed to install dvc hooks")
|
||||
self.repo.install(self.args.use_pre_commit_tool)
|
||||
except Exception: # noqa, pylint:disable=broad-except
|
||||
logger.exception("failed to install DVC Git hooks")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -27,4 +25,11 @@ def add_parser(subparsers, parent_parser):
|
||||
help=INSTALL_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
install_parser.add_argument(
|
||||
"--use-pre-commit-tool",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Install DVC hooks using pre-commit "
|
||||
"(https://pre-commit.com) if it is installed.",
|
||||
)
|
||||
install_parser.set_defaults(func=CmdInstall)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdLockBase(CmdBase):
|
||||
def _run(self, unlock):
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
self.repo.lock_stage(target, unlock=unlock)
|
||||
except DvcException:
|
||||
logger.exception(
|
||||
"failed to {}lock '{}'".format(
|
||||
"un" if unlock else "", target
|
||||
)
|
||||
)
|
||||
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
class CmdLock(CmdLockBase):
|
||||
def run(self):
|
||||
return self._run(False)
|
||||
|
||||
|
||||
class CmdUnlock(CmdLockBase):
|
||||
def run(self):
|
||||
return self._run(True)
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
LOCK_HELP = "Lock DVC-files."
|
||||
lock_parser = subparsers.add_parser(
|
||||
"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 to lock.")
|
||||
lock_parser.set_defaults(func=CmdLock)
|
||||
|
||||
UNLOCK_HELP = "Unlock DVC-files."
|
||||
unlock_parser = subparsers.add_parser(
|
||||
"unlock",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(UNLOCK_HELP, "unlock"),
|
||||
help=UNLOCK_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
unlock_parser.add_argument(
|
||||
"targets", nargs="+", help="DVC-files to unlock."
|
||||
)
|
||||
unlock_parser.set_defaults(func=CmdUnlock)
|
||||
@@ -0,0 +1,79 @@
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.command.ls.ls_colors import LsColors
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _prettify(entries, with_color=False):
|
||||
if with_color:
|
||||
ls_colors = LsColors()
|
||||
fmt = ls_colors.format
|
||||
else:
|
||||
|
||||
def fmt(entry):
|
||||
return entry["path"]
|
||||
|
||||
return [fmt(entry) for entry in entries]
|
||||
|
||||
|
||||
class CmdList(CmdBaseNoRepo):
|
||||
def run(self):
|
||||
from dvc.repo import Repo
|
||||
|
||||
try:
|
||||
entries = Repo.ls(
|
||||
self.args.url,
|
||||
self.args.path,
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
dvc_only=self.args.dvc_only,
|
||||
)
|
||||
if entries:
|
||||
entries = _prettify(entries, sys.stdout.isatty())
|
||||
logger.info("\n".join(entries))
|
||||
return 0
|
||||
except DvcException:
|
||||
logger.exception(f"failed to list '{self.args.url}'")
|
||||
return 1
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
LIST_HELP = (
|
||||
"List repository contents, including files"
|
||||
" and directories tracked by DVC and by Git."
|
||||
)
|
||||
list_parser = subparsers.add_parser(
|
||||
"list",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(LIST_HELP, "list"),
|
||||
help=LIST_HELP,
|
||||
formatter_class=argparse.RawTextHelpFormatter,
|
||||
)
|
||||
list_parser.add_argument("url", help="Location of DVC repository to list")
|
||||
list_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
help="Recursively list files.",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--dvc-only", action="store_true", help="Show only DVC outputs."
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"--rev",
|
||||
nargs="?",
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
list_parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
help="Path to directory within the repository to list outputs for",
|
||||
).complete = completion.DIR
|
||||
list_parser.set_defaults(func=CmdList)
|
||||
@@ -0,0 +1,48 @@
|
||||
import os
|
||||
|
||||
|
||||
class LsColors:
|
||||
default = "rs=0:di=01;34:ex=01;32"
|
||||
|
||||
def __init__(self, lscolors=None):
|
||||
self._extensions = {}
|
||||
self._codes = {}
|
||||
self._load(lscolors or os.environ.get("LS_COLORS") or LsColors.default)
|
||||
|
||||
def _load(self, lscolors):
|
||||
for item in lscolors.split(":"):
|
||||
try:
|
||||
code, color = item.split("=", 1)
|
||||
except ValueError:
|
||||
continue
|
||||
if code.startswith("*."):
|
||||
self._extensions[code[1:]] = color
|
||||
else:
|
||||
self._codes[code] = color
|
||||
|
||||
def format(self, entry):
|
||||
text = entry["path"]
|
||||
|
||||
if entry.get("isout", False) and "out" in self._codes:
|
||||
return self._format(text, code="out")
|
||||
|
||||
if entry.get("isdir", False):
|
||||
return self._format(text, code="di")
|
||||
|
||||
if entry.get("isexec", False):
|
||||
return self._format(text, code="ex")
|
||||
|
||||
_, ext = os.path.splitext(text)
|
||||
return self._format(text, ext=ext)
|
||||
|
||||
def _format(self, text, code=None, ext=None):
|
||||
val = None
|
||||
if ext:
|
||||
val = self._extensions.get(ext, None)
|
||||
if code:
|
||||
val = self._codes.get(code, None)
|
||||
|
||||
if not val:
|
||||
return text
|
||||
rs = self._codes.get("rs", 0)
|
||||
return f"\033[{val}m{text}\033[{rs}m"
|
||||
+180
-107
@@ -1,41 +1,38 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command.base import fix_subparsers
|
||||
from dvc.exceptions import BadMetricError
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import BadMetricError, DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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`
|
||||
"""
|
||||
DEFAULT_PRECISION = 5
|
||||
|
||||
|
||||
def show_metrics(
|
||||
metrics, all_branches=False, all_tags=False, all_commits=False
|
||||
):
|
||||
from flatten_json import flatten
|
||||
from dvc.utils.diff import format_dict
|
||||
|
||||
# When `metrics` contains a `None` key, it means that some files
|
||||
# specified as `targets` in `repo.metrics.show` didn't contain any metrics.
|
||||
missing = metrics.pop(None, None)
|
||||
|
||||
for branch, val in metrics.items():
|
||||
if all_branches or all_tags:
|
||||
logger.info("{branch}:".format(branch=branch))
|
||||
if all_branches or all_tags or all_commits:
|
||||
logger.info(f"{branch}:")
|
||||
|
||||
for fname, metric in val.items():
|
||||
lines = metric if type(metric) is list else metric.splitlines()
|
||||
if not isinstance(metric, dict):
|
||||
logger.info("\t{}: {}".format(fname, str(metric)))
|
||||
continue
|
||||
|
||||
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))
|
||||
logger.info(f"\t{fname}:")
|
||||
for key, value in flatten(format_dict(metric), ".").items():
|
||||
logger.info(f"\t\t{key}: {value}")
|
||||
|
||||
if missing:
|
||||
raise BadMetricError(missing)
|
||||
@@ -46,14 +43,23 @@ class CmdMetricsShow(CmdBase):
|
||||
try:
|
||||
metrics = self.repo.metrics.show(
|
||||
self.args.targets,
|
||||
typ=self.args.type,
|
||||
xpath=self.args.xpath,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
recursive=self.args.recursive,
|
||||
)
|
||||
|
||||
show_metrics(metrics, self.args.all_branches, self.args.all_tags)
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(metrics))
|
||||
else:
|
||||
show_metrics(
|
||||
metrics,
|
||||
self.args.all_branches,
|
||||
self.args.all_tags,
|
||||
self.args.all_commits,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to show metrics")
|
||||
return 1
|
||||
@@ -61,47 +67,78 @@ class CmdMetricsShow(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMetricsModify(CmdBase):
|
||||
def _show_diff(diff, markdown=False, no_path=False, old=False, precision=None):
|
||||
from collections import OrderedDict
|
||||
|
||||
from dvc.utils.diff import table
|
||||
|
||||
if precision is None:
|
||||
precision = DEFAULT_PRECISION
|
||||
|
||||
def _round(val):
|
||||
if isinstance(val, float):
|
||||
return round(val, precision)
|
||||
|
||||
return val
|
||||
|
||||
rows = []
|
||||
for fname, mdiff in diff.items():
|
||||
sorted_mdiff = OrderedDict(sorted(mdiff.items()))
|
||||
for metric, change in sorted_mdiff.items():
|
||||
row = [] if no_path else [fname]
|
||||
row.append(metric)
|
||||
if old:
|
||||
row.append(_round(change.get("old")))
|
||||
row.append(_round(change["new"]))
|
||||
row.append(_round(change.get("diff", "diff not supported")))
|
||||
rows.append(row)
|
||||
|
||||
header = [] if no_path else ["Path"]
|
||||
header.append("Metric")
|
||||
if old:
|
||||
header.extend(["Old", "New"])
|
||||
else:
|
||||
header.append("Value")
|
||||
header.append("Change")
|
||||
|
||||
return table(header, rows, markdown)
|
||||
|
||||
|
||||
class CmdMetricsDiff(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
self.repo.metrics.modify(
|
||||
self.args.path, typ=self.args.type, xpath=self.args.xpath
|
||||
diff = self.repo.metrics.diff(
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
targets=self.args.targets,
|
||||
recursive=self.args.recursive,
|
||||
all=self.args.all,
|
||||
)
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(diff))
|
||||
else:
|
||||
table = _show_diff(
|
||||
diff,
|
||||
self.args.show_md,
|
||||
self.args.no_path,
|
||||
self.args.old,
|
||||
precision=self.args.precision,
|
||||
)
|
||||
if table:
|
||||
logger.info(table)
|
||||
|
||||
except DvcException:
|
||||
logger.exception("failed to modify metric file settings")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMetricsAdd(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
self.repo.metrics.add(
|
||||
self.args.path, self.args.type, self.args.xpath
|
||||
)
|
||||
except DvcException:
|
||||
msg = "failed to add metric file '{}'".format(self.args.path)
|
||||
logger.exception(msg)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMetricsRemove(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
self.repo.metrics.remove(self.args.path)
|
||||
except DvcException:
|
||||
msg = "failed to remove metric file '{}'".format(self.args.path)
|
||||
logger.exception(msg)
|
||||
logger.exception("failed to show metrics diff")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
METRICS_HELP = "Commands to add, manage, collect and display metrics."
|
||||
METRICS_HELP = "Commands to display and compare metrics."
|
||||
|
||||
metrics_parser = subparsers.add_parser(
|
||||
"metrics",
|
||||
@@ -113,12 +150,12 @@ def add_parser(subparsers, parent_parser):
|
||||
|
||||
metrics_subparsers = metrics_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use dvc metrics CMD --help to display command-specific help.",
|
||||
help="Use `dvc metrics CMD --help` to display command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(metrics_subparsers)
|
||||
|
||||
METRICS_SHOW_HELP = "Output metric values."
|
||||
METRICS_SHOW_HELP = "Print metrics, with optional formatting."
|
||||
metrics_show_parser = metrics_subparsers.add_parser(
|
||||
"show",
|
||||
parents=[parent_parser],
|
||||
@@ -129,21 +166,11 @@ def add_parser(subparsers, parent_parser):
|
||||
metrics_show_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Metric files or directories (see -R) to show "
|
||||
"(leave empty to display all)",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"-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."
|
||||
"Limit command scope to these metric files. Using -R, "
|
||||
"directories to search metric files in can also be given."
|
||||
),
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
|
||||
)
|
||||
).complete = completion.FILE
|
||||
metrics_show_parser.add_argument(
|
||||
"-a",
|
||||
"--all-branches",
|
||||
@@ -158,6 +185,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Show metrics for all tags.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--all-commits",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show metrics for all commits.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_show_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
@@ -170,47 +209,81 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
metrics_show_parser.set_defaults(func=CmdMetricsShow)
|
||||
|
||||
METRICS_ADD_HELP = "Tag file as a metric file."
|
||||
metrics_add_parser = metrics_subparsers.add_parser(
|
||||
"add",
|
||||
METRICS_DIFF_HELP = (
|
||||
"Show changes in metrics between commits in the DVC repository, or "
|
||||
"between a commit and the workspace."
|
||||
)
|
||||
metrics_diff_parser = metrics_subparsers.add_parser(
|
||||
"diff",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(METRICS_ADD_HELP, "metrics/add"),
|
||||
help=METRICS_ADD_HELP,
|
||||
description=append_doc_link(METRICS_DIFF_HELP, "metrics/diff"),
|
||||
help=METRICS_DIFF_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
metrics_add_parser.add_argument(
|
||||
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
|
||||
metrics_diff_parser.add_argument(
|
||||
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
|
||||
)
|
||||
metrics_add_parser.add_argument(
|
||||
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
|
||||
metrics_diff_parser.add_argument(
|
||||
"b_rev",
|
||||
nargs="?",
|
||||
help="New Git commit to compare (defaults to the current workspace)",
|
||||
)
|
||||
metrics_add_parser.add_argument("path", help="Path to a metric file.")
|
||||
metrics_add_parser.set_defaults(func=CmdMetricsAdd)
|
||||
|
||||
METRICS_MODIFY_HELP = "Modify metric file options."
|
||||
metrics_modify_parser = metrics_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(METRICS_MODIFY_HELP, "metrics/modify"),
|
||||
help=METRICS_MODIFY_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
metrics_diff_parser.add_argument(
|
||||
"--targets",
|
||||
nargs="*",
|
||||
help=(
|
||||
"Limit command scope to these metric files. Using -R, "
|
||||
"directories to search metric files in can also be given."
|
||||
),
|
||||
metavar="<paths>",
|
||||
).complete = completion.FILE
|
||||
metrics_diff_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"If any target is a directory, recursively search and process "
|
||||
"metric files."
|
||||
),
|
||||
)
|
||||
metrics_modify_parser.add_argument(
|
||||
"-t", "--type", help="Type of metrics (raw/json/tsv/htsv/csv/hcsv)."
|
||||
metrics_diff_parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show unchanged metrics as well.",
|
||||
)
|
||||
metrics_modify_parser.add_argument(
|
||||
"-x", "--xpath", help="json/tsv/htsv/csv/hcsv path."
|
||||
metrics_diff_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
metrics_modify_parser.add_argument("path", help="Path to a metric file.")
|
||||
metrics_modify_parser.set_defaults(func=CmdMetricsModify)
|
||||
|
||||
METRICS_REMOVE_HELP = "Remove files's metric tag."
|
||||
metrics_remove_parser = metrics_subparsers.add_parser(
|
||||
"remove",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(METRICS_REMOVE_HELP, "metrics/remove"),
|
||||
help=METRICS_REMOVE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
metrics_diff_parser.add_argument(
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
metrics_remove_parser.add_argument("path", help="Path to a metric file.")
|
||||
metrics_remove_parser.set_defaults(func=CmdMetricsRemove)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--old",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show old metric value.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--no-path",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't show metric path.",
|
||||
)
|
||||
metrics_diff_parser.add_argument(
|
||||
"--precision",
|
||||
type=int,
|
||||
help=(
|
||||
"Round metrics to `n` digits precision after the decimal point. "
|
||||
f"Rounds to {DEFAULT_PRECISION} digits by default."
|
||||
),
|
||||
metavar="<n>",
|
||||
)
|
||||
metrics_diff_parser.set_defaults(func=CmdMetricsDiff)
|
||||
|
||||
+7
-6
@@ -1,11 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -38,7 +37,9 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
move_parser.add_argument(
|
||||
"src", help="Source path to a data file or directory."
|
||||
)
|
||||
move_parser.add_argument("dst", help="Destination path.")
|
||||
"src", help="Source path to a data file or directory.",
|
||||
).complete = completion.FILE
|
||||
move_parser.add_argument(
|
||||
"dst", help="Destination path.",
|
||||
).complete = completion.FILE
|
||||
move_parser.set_defaults(func=CmdMove)
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import argparse
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _show_diff(diff, markdown=False, no_path=False):
|
||||
from dvc.utils.diff import table
|
||||
|
||||
rows = []
|
||||
for fname, pdiff in diff.items():
|
||||
sorted_pdiff = OrderedDict(sorted(pdiff.items()))
|
||||
for param, change in sorted_pdiff.items():
|
||||
row = [] if no_path else [fname]
|
||||
row.append(param)
|
||||
row.append(change["old"])
|
||||
row.append(change["new"])
|
||||
rows.append(row)
|
||||
|
||||
header = [] if no_path else ["Path"]
|
||||
header.append("Param")
|
||||
header.append("Old")
|
||||
header.append("New")
|
||||
|
||||
return table(header, rows, markdown)
|
||||
|
||||
|
||||
class CmdParamsDiff(CmdBase):
|
||||
def run(self):
|
||||
try:
|
||||
diff = self.repo.params.diff(
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
all=self.args.all,
|
||||
)
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(diff))
|
||||
else:
|
||||
table = _show_diff(diff, self.args.show_md, self.args.no_path)
|
||||
if table:
|
||||
logger.info(table)
|
||||
|
||||
except DvcException:
|
||||
logger.exception("failed to show params diff")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
PARAMS_HELP = "Commands to display params."
|
||||
|
||||
params_parser = subparsers.add_parser(
|
||||
"params",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PARAMS_HELP, "params"),
|
||||
help=PARAMS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
params_subparsers = params_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc params CMD --help` to display command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(params_subparsers)
|
||||
|
||||
PARAMS_DIFF_HELP = (
|
||||
"Show changes in params between commits in the DVC repository, or "
|
||||
"between a commit and the workspace."
|
||||
)
|
||||
params_diff_parser = params_subparsers.add_parser(
|
||||
"diff",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PARAMS_DIFF_HELP, "params/diff"),
|
||||
help=PARAMS_DIFF_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"a_rev", nargs="?", help="Old Git commit to compare (defaults to HEAD)"
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"b_rev",
|
||||
nargs="?",
|
||||
help=("New Git commit to compare (defaults to the current workspace)"),
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--all",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show unchanged params as well.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--show-json",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in JSON format.",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
params_diff_parser.add_argument(
|
||||
"--no-path",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Don't show params path.",
|
||||
)
|
||||
params_diff_parser.set_defaults(func=CmdParamsDiff)
|
||||
@@ -1,244 +0,0 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdPipelineShow(CmdBase):
|
||||
def _show(self, target, commands, outs, locked):
|
||||
import networkx
|
||||
from dvc.stage import Stage
|
||||
|
||||
stage = Stage.load(self.repo, target)
|
||||
G = self.repo.graph
|
||||
stages = networkx.dfs_postorder_nodes(G, stage)
|
||||
|
||||
if locked:
|
||||
stages = [s for s in stages if s.locked]
|
||||
|
||||
for stage in stages:
|
||||
if commands:
|
||||
if stage.cmd is None:
|
||||
continue
|
||||
logger.info(stage.cmd)
|
||||
elif outs:
|
||||
for out in stage.outs:
|
||||
logger.info(str(out))
|
||||
else:
|
||||
logger.info(stage.path_in_repo)
|
||||
|
||||
def __build_graph(self, target, commands, outs):
|
||||
import networkx
|
||||
from dvc.stage import Stage
|
||||
from dvc.repo.graph import get_pipeline
|
||||
|
||||
stage = Stage.load(self.repo, target)
|
||||
G = get_pipeline(self.repo.pipelines, stage)
|
||||
|
||||
nodes = []
|
||||
for stage in G:
|
||||
if commands:
|
||||
if stage.cmd is None:
|
||||
continue
|
||||
nodes.append(stage.cmd)
|
||||
elif outs:
|
||||
for out in stage.outs:
|
||||
nodes.append(str(out))
|
||||
else:
|
||||
nodes.append(stage.relpath)
|
||||
|
||||
edges = []
|
||||
for from_stage, to_stage in G.edges():
|
||||
if commands:
|
||||
if to_stage.cmd is None:
|
||||
continue
|
||||
edges.append((from_stage.cmd, to_stage.cmd))
|
||||
elif outs:
|
||||
for from_out in from_stage.outs:
|
||||
for to_out in to_stage.outs:
|
||||
edges.append((str(from_out), str(to_out)))
|
||||
else:
|
||||
edges.append((from_stage.relpath, to_stage.relpath))
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
import io
|
||||
import networkx
|
||||
from networkx.drawing.nx_pydot import write_dot
|
||||
|
||||
_, edges, _ = self.__build_graph(target, commands, outs)
|
||||
edges = [edge[::-1] for edge in edges]
|
||||
|
||||
simple_g = networkx.DiGraph()
|
||||
simple_g.add_edges_from(edges)
|
||||
|
||||
dot_file = io.StringIO()
|
||||
write_dot(simple_g, dot_file)
|
||||
logger.info(dot_file.getvalue())
|
||||
|
||||
def run(self):
|
||||
if not self.args.targets:
|
||||
self.args.targets = self.default_targets
|
||||
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
if self.args.ascii:
|
||||
self._show_ascii(
|
||||
target, self.args.commands, self.args.outs
|
||||
)
|
||||
elif self.args.dot:
|
||||
self.__write_dot(
|
||||
target, self.args.commands, self.args.outs
|
||||
)
|
||||
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,
|
||||
self.args.locked,
|
||||
)
|
||||
except DvcException:
|
||||
msg = "failed to show pipeline for '{}'".format(target)
|
||||
logger.exception(msg)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
class CmdPipelineList(CmdBase):
|
||||
def run(self):
|
||||
pipelines = self.repo.pipelines
|
||||
for pipeline in pipelines:
|
||||
for stage in pipeline:
|
||||
logger.info(stage.relpath)
|
||||
if len(pipeline) != 0:
|
||||
logger.info("=" * 80)
|
||||
logger.info("{} pipelines total".format(len(pipelines)))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
PIPELINE_HELP = "Manage pipelines."
|
||||
pipeline_parser = subparsers.add_parser(
|
||||
"pipeline",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PIPELINE_HELP, "pipeline"),
|
||||
help=PIPELINE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
pipeline_subparsers = pipeline_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use dvc pipeline CMD --help for command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(pipeline_subparsers)
|
||||
|
||||
PIPELINE_SHOW_HELP = "Show pipelines."
|
||||
pipeline_show_parser = pipeline_subparsers.add_parser(
|
||||
"show",
|
||||
parents=[parent_parser],
|
||||
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(
|
||||
"-c",
|
||||
"--commands",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print commands instead of paths to DVC-files.",
|
||||
)
|
||||
pipeline_show_group.add_argument(
|
||||
"-o",
|
||||
"--outs",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print output files instead of paths to DVC-files.",
|
||||
)
|
||||
pipeline_show_parser.add_argument(
|
||||
"-l",
|
||||
"--locked",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print locked DVC stages",
|
||||
)
|
||||
pipeline_show_parser.add_argument(
|
||||
"--ascii",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Output DAG as ASCII.",
|
||||
)
|
||||
pipeline_show_parser.add_argument(
|
||||
"--dot",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print DAG with .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 to show pipeline for. Optional. "
|
||||
"(Finds all DVC-files in the workspace by default.)",
|
||||
)
|
||||
pipeline_show_parser.set_defaults(func=CmdPipelineShow)
|
||||
|
||||
PIPELINE_LIST_HELP = "List pipelines."
|
||||
pipeline_list_parser = pipeline_subparsers.add_parser(
|
||||
"list",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PIPELINE_LIST_HELP, "pipeline/list"),
|
||||
help=PIPELINE_LIST_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
pipeline_list_parser.set_defaults(func=CmdPipelineList)
|
||||
@@ -0,0 +1,233 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.schema import PLOT_PROPS
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PAGE_HTML = """<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>DVC Plot</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
|
||||
</head>
|
||||
<body>
|
||||
{divs}
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
DIV_HTML = """<div id = "{id}"></div>
|
||||
<script type = "text/javascript">
|
||||
var spec = {vega_json};
|
||||
vegaEmbed('#{id}', spec);
|
||||
</script>"""
|
||||
|
||||
|
||||
class CmdPlots(CmdBase):
|
||||
def _func(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def _props(self):
|
||||
# Pass only props specified by user, to not shadow ones from plot def
|
||||
props = {p: getattr(self.args, p) for p in PLOT_PROPS}
|
||||
return {k: v for k, v in props.items() if v is not None}
|
||||
|
||||
def run(self):
|
||||
if self.args.show_vega:
|
||||
if not self.args.targets:
|
||||
logger.error("please specify a target for `--show-vega`")
|
||||
return 1
|
||||
if len(self.args.targets) > 1:
|
||||
logger.error(
|
||||
"you can only specify one target for `--show-vega`"
|
||||
)
|
||||
return 1
|
||||
|
||||
try:
|
||||
plots = self._func(targets=self.args.targets, props=self._props())
|
||||
|
||||
if self.args.show_vega:
|
||||
target = self.args.targets[0]
|
||||
logger.info(plots[target])
|
||||
return 0
|
||||
|
||||
divs = [
|
||||
DIV_HTML.format(id=f"plot{i}", vega_json=plot)
|
||||
for i, plot in enumerate(plots.values())
|
||||
]
|
||||
html = PAGE_HTML.format(divs="\n".join(divs))
|
||||
path = self.args.out or "plots.html"
|
||||
|
||||
with open(path, "w") as fobj:
|
||||
fobj.write(html)
|
||||
|
||||
logger.info(
|
||||
"file://{}".format(os.path.join(self.repo.root_dir, path))
|
||||
)
|
||||
|
||||
except DvcException:
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdPlotsShow(CmdPlots):
|
||||
def _func(self, *args, **kwargs):
|
||||
return self.repo.plots.show(*args, **kwargs)
|
||||
|
||||
|
||||
class CmdPlotsDiff(CmdPlots):
|
||||
def _func(self, *args, **kwargs):
|
||||
return self.repo.plots.diff(*args, revs=self.args.revisions, **kwargs)
|
||||
|
||||
|
||||
class CmdPlotsModify(CmdPlots):
|
||||
def run(self):
|
||||
self.repo.plots.modify(
|
||||
self.args.target, props=self._props(), unset=self.args.unset,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
PLOTS_HELP = (
|
||||
"Commands to visualize and compare plot metrics in structured files "
|
||||
"(JSON, YAML, CSV, TSV)"
|
||||
)
|
||||
|
||||
plots_parser = subparsers.add_parser(
|
||||
"plots",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PLOTS_HELP, "plots"),
|
||||
help=PLOTS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_subparsers = plots_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use `dvc plots CMD --help` to display command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(plots_subparsers)
|
||||
|
||||
SHOW_HELP = "Generate plots from metric files."
|
||||
plots_show_parser = plots_subparsers.add_parser(
|
||||
"show",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(SHOW_HELP, "plots/show"),
|
||||
help=SHOW_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_show_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="Plots files to visualize. Shows all plots by default.",
|
||||
).complete = completion.FILE
|
||||
_add_props_arguments(plots_show_parser)
|
||||
_add_output_arguments(plots_show_parser)
|
||||
plots_show_parser.set_defaults(func=CmdPlotsShow)
|
||||
|
||||
PLOTS_DIFF_HELP = (
|
||||
"Show multiple versions of plot metrics "
|
||||
"by plotting them in a single image."
|
||||
)
|
||||
plots_diff_parser = plots_subparsers.add_parser(
|
||||
"diff",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PLOTS_DIFF_HELP, "plots/diff"),
|
||||
help=PLOTS_DIFF_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_diff_parser.add_argument(
|
||||
"--targets",
|
||||
nargs="*",
|
||||
help="Plots file to visualize. Shows all plots by default.",
|
||||
metavar="<path>",
|
||||
).complete = completion.FILE
|
||||
plots_diff_parser.add_argument(
|
||||
"revisions", nargs="*", default=None, help="Git commits to plot from",
|
||||
)
|
||||
_add_props_arguments(plots_diff_parser)
|
||||
_add_output_arguments(plots_diff_parser)
|
||||
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
||||
|
||||
PLOTS_MODIFY_HELP = "Modify display properties of plot metric files."
|
||||
plots_modify_parser = plots_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_parser],
|
||||
description=append_doc_link(PLOTS_MODIFY_HELP, "plots/modify"),
|
||||
help=PLOTS_MODIFY_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
plots_modify_parser.add_argument(
|
||||
"target", help="Metric file to set properties to",
|
||||
).complete = completion.FILE
|
||||
_add_props_arguments(plots_modify_parser)
|
||||
plots_modify_parser.add_argument(
|
||||
"--unset",
|
||||
nargs="*",
|
||||
metavar="<property>",
|
||||
help="Unset one or more display properties.",
|
||||
)
|
||||
plots_modify_parser.set_defaults(func=CmdPlotsModify)
|
||||
|
||||
|
||||
def _add_props_arguments(parser):
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--template",
|
||||
nargs="?",
|
||||
default=None,
|
||||
help=(
|
||||
"Special JSON or HTML schema file to inject with the data. "
|
||||
"See {}".format(
|
||||
format_link("https://man.dvc.org/plots#plot-templates")
|
||||
)
|
||||
),
|
||||
metavar="<path>",
|
||||
).complete = completion.FILE
|
||||
parser.add_argument(
|
||||
"-x", default=None, help="Field name for X axis.", metavar="<field>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-y", default=None, help="Field name for Y axis.", metavar="<field>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-header",
|
||||
action="store_false",
|
||||
dest="header",
|
||||
default=None, # Use default None to distinguish when it's not used
|
||||
help="Provided CSV or TSV datafile does not have a header.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--title", default=None, metavar="<text>", help="Plot title."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--x-label", default=None, help="X axis label", metavar="<text>"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--y-label", default=None, help="Y axis label", metavar="<text>"
|
||||
)
|
||||
|
||||
|
||||
def _add_output_arguments(parser):
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--out",
|
||||
default=None,
|
||||
help="Destination path to save plots to",
|
||||
metavar="<path>",
|
||||
).complete = completion.DIR
|
||||
parser.add_argument(
|
||||
"--show-vega",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show output in Vega format.",
|
||||
)
|
||||
+137
-56
@@ -1,78 +1,146 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import fix_subparsers
|
||||
from dvc.command.base import append_doc_link, fix_subparsers
|
||||
from dvc.command.config import CmdConfig
|
||||
from dvc.remote.config import RemoteConfig
|
||||
from dvc.config import ConfigError
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRemoteConfig(CmdConfig):
|
||||
class CmdRemote(CmdConfig):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
self.remote_config = RemoteConfig(self.config)
|
||||
|
||||
if getattr(self.args, "name", None):
|
||||
self.args.name = self.args.name.lower()
|
||||
|
||||
def _check_exists(self, conf):
|
||||
if self.args.name not in conf["remote"]:
|
||||
raise ConfigError(f"remote '{self.args.name}' doesn't exists.")
|
||||
|
||||
|
||||
class CmdRemoteAdd(CmdRemoteConfig):
|
||||
class CmdRemoteAdd(CmdRemote):
|
||||
def run(self):
|
||||
if self.args.default:
|
||||
logger.info(
|
||||
"Setting '{}' as a default remote.".format(self.args.name)
|
||||
)
|
||||
self.remote_config.add(
|
||||
self.args.name,
|
||||
self.args.url,
|
||||
force=self.args.force,
|
||||
default=self.args.default,
|
||||
level=self.args.level,
|
||||
)
|
||||
logger.info(f"Setting '{self.args.name}' as a default remote.")
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.name in conf["remote"] and not self.args.force:
|
||||
raise ConfigError(
|
||||
"remote '{}' already exists. Use `-f|--force` to "
|
||||
"overwrite it.".format(self.args.name)
|
||||
)
|
||||
|
||||
conf["remote"][self.args.name] = {"url": self.args.url}
|
||||
if self.args.default:
|
||||
conf["core"]["remote"] = self.args.name
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdRemoteRemove(CmdRemoteConfig):
|
||||
class CmdRemoteRemove(CmdRemote):
|
||||
def run(self):
|
||||
self.remote_config.remove(self.args.name, level=self.args.level)
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
self._check_exists(conf)
|
||||
del conf["remote"][self.args.name]
|
||||
|
||||
# Remove core.remote refs to this remote in any shadowing configs
|
||||
for level in reversed(self.config.LEVELS):
|
||||
with self.config.edit(level) as conf:
|
||||
if conf["core"].get("remote") == self.args.name:
|
||||
del conf["core"]["remote"]
|
||||
|
||||
if level == self.args.level:
|
||||
break
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdRemoteModify(CmdRemoteConfig):
|
||||
class CmdRemoteModify(CmdRemote):
|
||||
def run(self):
|
||||
self.remote_config.modify(
|
||||
self.args.name,
|
||||
self.args.option,
|
||||
self.args.value,
|
||||
level=self.args.level,
|
||||
)
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
self._check_exists(conf)
|
||||
section = conf["remote"][self.args.name]
|
||||
if self.args.unset:
|
||||
section.pop(self.args.option, None)
|
||||
else:
|
||||
section[self.args.option] = self.args.value
|
||||
return 0
|
||||
|
||||
|
||||
class CmdRemoteDefault(CmdRemoteConfig):
|
||||
class CmdRemoteDefault(CmdRemote):
|
||||
def run(self):
|
||||
|
||||
if self.args.name is None and not self.args.unset:
|
||||
name = self.remote_config.get_default(level=self.args.level)
|
||||
print(name)
|
||||
conf = self.config.load_one(self.args.level)
|
||||
try:
|
||||
print(conf["core"]["remote"])
|
||||
except KeyError:
|
||||
logger.info("No default remote set")
|
||||
return 1
|
||||
else:
|
||||
self.remote_config.set_default(
|
||||
self.args.name, unset=self.args.unset, level=self.args.level
|
||||
)
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
if self.args.unset:
|
||||
conf["core"].pop("remote", None)
|
||||
else:
|
||||
merged_conf = self.config.load_config_to_level(
|
||||
self.args.level
|
||||
)
|
||||
if (
|
||||
self.args.name in conf["remote"]
|
||||
or self.args.name in merged_conf["remote"]
|
||||
):
|
||||
conf["core"]["remote"] = self.args.name
|
||||
else:
|
||||
raise ConfigError(
|
||||
"default remote must be present in remote list."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
class CmdRemoteList(CmdRemoteConfig):
|
||||
class CmdRemoteList(CmdRemote):
|
||||
def run(self):
|
||||
for name, url in self.remote_config.list(
|
||||
level=self.args.level
|
||||
).items():
|
||||
logger.info("{}\t{}".format(name, url))
|
||||
conf = self.config.load_one(self.args.level)
|
||||
for name, conf in conf["remote"].items():
|
||||
logger.info("{}\t{}".format(name, conf["url"]))
|
||||
return 0
|
||||
|
||||
|
||||
class CmdRemoteRename(CmdRemote):
|
||||
def _rename_default(self, conf):
|
||||
if conf["core"].get("remote") == self.args.name:
|
||||
conf["core"]["remote"] = self.args.new
|
||||
|
||||
def run(self):
|
||||
all_config = self.config.load_config_to_level(None)
|
||||
if self.args.new in all_config.get("remote", {}):
|
||||
raise ConfigError(
|
||||
"Rename failed. Remote name '{}' already exists.".format(
|
||||
{self.args.new}
|
||||
)
|
||||
)
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
self._check_exists(conf)
|
||||
conf["remote"][self.args.new] = conf["remote"][self.args.name]
|
||||
del conf["remote"][self.args.name]
|
||||
self._rename_default(conf)
|
||||
|
||||
for level in reversed(self.config.LEVELS):
|
||||
if level == self.args.level:
|
||||
break
|
||||
with self.config.edit(level) as level_conf:
|
||||
self._rename_default(level_conf)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
from dvc.command.config import parent_config_parser
|
||||
|
||||
REMOTE_HELP = "Manage remote storage configuration."
|
||||
REMOTE_HELP = "Set up and manage data remotes."
|
||||
remote_parser = subparsers.add_parser(
|
||||
"remote",
|
||||
parents=[parent_parser],
|
||||
@@ -83,12 +151,12 @@ def add_parser(subparsers, parent_parser):
|
||||
|
||||
remote_subparsers = remote_parser.add_subparsers(
|
||||
dest="cmd",
|
||||
help="Use dvc remote CMD --help for " "command-specific help.",
|
||||
help="Use `dvc remote CMD --help` for " "command-specific help.",
|
||||
)
|
||||
|
||||
fix_subparsers(remote_subparsers)
|
||||
|
||||
REMOTE_ADD_HELP = "Add remote."
|
||||
REMOTE_ADD_HELP = "Add a new data remote."
|
||||
remote_add_parser = remote_subparsers.add_parser(
|
||||
"add",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
@@ -99,7 +167,9 @@ def add_parser(subparsers, parent_parser):
|
||||
remote_add_parser.add_argument("name", help="Name of the remote")
|
||||
remote_add_parser.add_argument(
|
||||
"url",
|
||||
help="URL. See full list of supported urls at " "man.dvc.org/remote",
|
||||
help="URL. See full list of supported urls at {}".format(
|
||||
format_link("https://man.dvc.org/remote")
|
||||
),
|
||||
)
|
||||
remote_add_parser.add_argument(
|
||||
"-d",
|
||||
@@ -117,7 +187,7 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
remote_add_parser.set_defaults(func=CmdRemoteAdd)
|
||||
|
||||
REMOTE_DEFAULT_HELP = "Set/unset default remote."
|
||||
REMOTE_DEFAULT_HELP = "Set/unset the default data remote."
|
||||
remote_default_parser = remote_subparsers.add_parser(
|
||||
"default",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
@@ -137,20 +207,7 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
remote_default_parser.set_defaults(func=CmdRemoteDefault)
|
||||
|
||||
REMOTE_REMOVE_HELP = "Remove remote."
|
||||
remote_remove_parser = remote_subparsers.add_parser(
|
||||
"remove",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
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 of the remote to remove."
|
||||
)
|
||||
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
|
||||
|
||||
REMOTE_MODIFY_HELP = "Modify remote."
|
||||
REMOTE_MODIFY_HELP = "Modify the configuration of a data remote."
|
||||
remote_modify_parser = remote_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
@@ -174,7 +231,7 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
remote_modify_parser.set_defaults(func=CmdRemoteModify)
|
||||
|
||||
REMOTE_LIST_HELP = "List available remotes."
|
||||
REMOTE_LIST_HELP = "List all available data remotes."
|
||||
remote_list_parser = remote_subparsers.add_parser(
|
||||
"list",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
@@ -183,3 +240,27 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_list_parser.set_defaults(func=CmdRemoteList)
|
||||
|
||||
REMOTE_REMOVE_HELP = "Remove a data remote."
|
||||
remote_remove_parser = remote_subparsers.add_parser(
|
||||
"remove",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
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 of the remote to remove."
|
||||
)
|
||||
remote_remove_parser.set_defaults(func=CmdRemoteRemove)
|
||||
REMOTE_RENAME_HELP = "Rename a DVC remote"
|
||||
remote_rename_parser = remote_subparsers.add_parser(
|
||||
"rename",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(REMOTE_RENAME_HELP, "remote/rename"),
|
||||
help=REMOTE_RENAME_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remote_rename_parser.add_argument("name", help="Remote to be renamed")
|
||||
remote_rename_parser.add_argument("new", help="New name of the remote")
|
||||
remote_rename_parser.set_defaults(func=CmdRemoteRename)
|
||||
|
||||
+11
-47
@@ -1,48 +1,28 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRemove(CmdBase):
|
||||
def _is_outs_only(self, target):
|
||||
if not self.args.purge:
|
||||
return True
|
||||
|
||||
if self.args.force:
|
||||
return False
|
||||
|
||||
msg = "Are you sure you want to remove '{}' with its outputs?".format(
|
||||
target
|
||||
)
|
||||
|
||||
if prompt.confirm(msg):
|
||||
return False
|
||||
|
||||
raise DvcException(
|
||||
"Cannot purge without a confirmation from the user."
|
||||
" Use `-f` to force."
|
||||
)
|
||||
|
||||
def run(self):
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
outs_only = self._is_outs_only(target)
|
||||
self.repo.remove(target, outs_only=outs_only)
|
||||
self.repo.remove(target, outs=self.args.outs)
|
||||
except DvcException:
|
||||
logger.exception("failed to remove '{}'".format(target))
|
||||
logger.exception(f"failed to remove '{target}'")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
REMOVE_HELP = "Remove DVC-file outputs."
|
||||
REMOVE_HELP = (
|
||||
"Remove stage entry, remove .gitignore entry and unprotect outputs"
|
||||
)
|
||||
remove_parser = subparsers.add_parser(
|
||||
"remove",
|
||||
parents=[parent_parser],
|
||||
@@ -50,29 +30,13 @@ def add_parser(subparsers, parent_parser):
|
||||
help=REMOVE_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
remove_parser_group = remove_parser.add_mutually_exclusive_group()
|
||||
remove_parser_group.add_argument(
|
||||
"-o",
|
||||
remove_parser.add_argument(
|
||||
"--outs",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Only remove DVC-file outputs. (Default)",
|
||||
)
|
||||
remove_parser_group.add_argument(
|
||||
"-p",
|
||||
"--purge",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Remove DVC-file and all its outputs.",
|
||||
help="Remove outputs as well.",
|
||||
)
|
||||
remove_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Force purge.",
|
||||
)
|
||||
remove_parser.add_argument(
|
||||
"targets", nargs="+", help="DVC-files to remove."
|
||||
)
|
||||
"targets", nargs="+", help="DVC-files to remove.",
|
||||
).complete = completion.DVC_FILE
|
||||
remove_parser.set_defaults(func=CmdRemove)
|
||||
|
||||
+21
-8
@@ -2,13 +2,13 @@ import argparse
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.command.metrics import show_metrics
|
||||
from dvc.command.status import CmdDataStatus
|
||||
from dvc.dvcfile import PIPELINE_FILE
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -35,10 +35,11 @@ class CmdRepro(CmdBase):
|
||||
interactive=self.args.interactive,
|
||||
pipeline=self.args.pipeline,
|
||||
all_pipelines=self.args.all_pipelines,
|
||||
ignore_build_cache=self.args.ignore_build_cache,
|
||||
run_cache=not self.args.no_run_cache,
|
||||
no_commit=self.args.no_commit,
|
||||
downstream=self.args.downstream,
|
||||
recursive=self.args.recursive,
|
||||
force_downstream=self.args.force_downstream,
|
||||
)
|
||||
|
||||
if len(stages) == 0:
|
||||
@@ -57,7 +58,9 @@ class CmdRepro(CmdBase):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
REPRO_HELP = "Check for changes and reproduce stages and dependencies."
|
||||
REPRO_HELP = (
|
||||
"Reproduce complete or partial pipelines by executing their stages."
|
||||
)
|
||||
repro_parser = subparsers.add_parser(
|
||||
"repro",
|
||||
parents=[parent_parser],
|
||||
@@ -68,8 +71,8 @@ def add_parser(subparsers, parent_parser):
|
||||
repro_parser.add_argument(
|
||||
"targets",
|
||||
nargs="*",
|
||||
help="DVC-file to reproduce. 'Dvcfile' by default.",
|
||||
)
|
||||
help=f"Stages to reproduce. '{PIPELINE_FILE}' by default.",
|
||||
).complete = completion.DVC_FILE
|
||||
repro_parser.add_argument(
|
||||
"-f",
|
||||
"--force",
|
||||
@@ -90,6 +93,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--cwd",
|
||||
default=os.path.curdir,
|
||||
help="Directory within your repo to reproduce from.",
|
||||
metavar="<path>",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"-m",
|
||||
@@ -135,7 +139,16 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Reproduce all stages in the specified directory.",
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--ignore-build-cache",
|
||||
"--no-run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Execute stage commands even if they have already been run with "
|
||||
"the same command/dependencies/outputs/etc before."
|
||||
),
|
||||
)
|
||||
repro_parser.add_argument(
|
||||
"--force-downstream",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Reproduce all descendants of a changed stage even if their "
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBaseNoRepo
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.utils import relpath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -17,7 +16,7 @@ class CmdRoot(CmdBaseNoRepo):
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
ROOT_HELP = "Relative path to the repository's directory."
|
||||
ROOT_HELP = "Return the relative path to the root of the DVC project."
|
||||
root_parser = subparsers.add_parser(
|
||||
"root",
|
||||
parents=[parent_parser],
|
||||
|
||||
+70
-36
@@ -1,18 +1,14 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdRun(CmdBase):
|
||||
def run(self):
|
||||
overwrite = self.args.yes or self.args.overwrite_dvcfile
|
||||
|
||||
if not any(
|
||||
[
|
||||
self.args.deps,
|
||||
@@ -20,15 +16,18 @@ class CmdRun(CmdBase):
|
||||
self.args.outs_no_cache,
|
||||
self.args.metrics,
|
||||
self.args.metrics_no_cache,
|
||||
self.args.plots,
|
||||
self.args.plots_no_cache,
|
||||
self.args.outs_persist,
|
||||
self.args.outs_persist_no_cache,
|
||||
self.args.params,
|
||||
self.args.command,
|
||||
]
|
||||
): # pragma: no cover
|
||||
logger.error(
|
||||
"too few arguments. Specify at least one: `-d`, `-o`, `-O`, "
|
||||
"`-m`, `-M`, `--outs-persist`, `--outs-persist-no-cache`, "
|
||||
"`command`."
|
||||
"`-m`, `-M`, `-p`, `--plots`, `--plots-no-cache`, "
|
||||
"`--outs-persist`, `--outs-persist-no-cache`, `command`."
|
||||
)
|
||||
return 1
|
||||
|
||||
@@ -39,21 +38,25 @@ class CmdRun(CmdBase):
|
||||
outs_no_cache=self.args.outs_no_cache,
|
||||
metrics=self.args.metrics,
|
||||
metrics_no_cache=self.args.metrics_no_cache,
|
||||
plots=self.args.plots,
|
||||
plots_no_cache=self.args.plots_no_cache,
|
||||
deps=self.args.deps,
|
||||
params=self.args.params,
|
||||
fname=self.args.file,
|
||||
cwd=self.args.cwd,
|
||||
wdir=self.args.wdir,
|
||||
no_exec=self.args.no_exec,
|
||||
overwrite=overwrite,
|
||||
ignore_build_cache=self.args.ignore_build_cache,
|
||||
remove_outs=self.args.remove_outs,
|
||||
force=self.args.force,
|
||||
run_cache=not self.args.no_run_cache,
|
||||
no_commit=self.args.no_commit,
|
||||
outs_persist=self.args.outs_persist,
|
||||
outs_persist_no_cache=self.args.outs_persist_no_cache,
|
||||
always_changed=self.args.always_changed,
|
||||
name=self.args.name,
|
||||
single_stage=self.args.single_stage,
|
||||
external=self.args.external,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed to run command")
|
||||
logger.exception("")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
@@ -77,7 +80,7 @@ class CmdRun(CmdBase):
|
||||
if " " not in argument or '"' in argument:
|
||||
return argument
|
||||
|
||||
return '"{}"'.format(argument)
|
||||
return f'"{argument}"'
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
@@ -95,6 +98,10 @@ def add_parser(subparsers, parent_parser):
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare dependencies for reproducible cmd.",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-n", "--name", help="Stage name.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-o",
|
||||
@@ -102,6 +109,7 @@ def add_parser(subparsers, parent_parser):
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output file or directory.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-O",
|
||||
@@ -110,32 +118,54 @@ def add_parser(subparsers, parent_parser):
|
||||
default=[],
|
||||
help="Declare output file or directory "
|
||||
"(do not put into DVC cache).",
|
||||
metavar="<filename>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-p",
|
||||
"--params",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare parameter to use as additional dependency.",
|
||||
metavar="[<filename>:]<params_list>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-m",
|
||||
"--metrics",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output metric file or directory.",
|
||||
help="Declare output metric file.",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-M",
|
||||
"--metrics-no-cache",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output metric file or directory "
|
||||
"(do not put into DVC cache).",
|
||||
help="Declare output metric file (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-f", "--file", help="Specify name of the DVC-file it generates."
|
||||
"--plots",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output plot file.",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-c", "--cwd", help="Deprecated, use -w and -f instead."
|
||||
"--plots-no-cache",
|
||||
action="append",
|
||||
default=[],
|
||||
help="Declare output plot file (do not put into DVC cache).",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--file", metavar="<filename>", help=argparse.SUPPRESS,
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-w",
|
||||
"--wdir",
|
||||
help="Directory within your repo to run your command in.",
|
||||
metavar="<path>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-exec",
|
||||
@@ -144,30 +174,20 @@ def add_parser(subparsers, parent_parser):
|
||||
help="Only create stage file without actually running it.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"-y",
|
||||
"--yes",
|
||||
"-f",
|
||||
"--force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Deprecated, use --overwrite-dvcfile instead",
|
||||
help="Overwrite existing stage",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--overwrite-dvcfile",
|
||||
"--no-run-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Overwrite existing DVC-file without asking for confirmation.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--ignore-build-cache",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Run this stage even if it has been already ran with the same "
|
||||
"command/dependencies/outputs/etc before.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--remove-outs",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Deprecated, this is now the default behavior",
|
||||
help=(
|
||||
"Execute the command even if this stage has already been run "
|
||||
"with the same command/dependencies/outputs/etc before."
|
||||
),
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--no-commit",
|
||||
@@ -181,6 +201,7 @@ def add_parser(subparsers, parent_parser):
|
||||
default=[],
|
||||
help="Declare output file or directory that will not be "
|
||||
"removed upon repro.",
|
||||
metavar="<filename>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--outs-persist-no-cache",
|
||||
@@ -188,6 +209,7 @@ def add_parser(subparsers, parent_parser):
|
||||
default=[],
|
||||
help="Declare output file or directory that will not be "
|
||||
"removed upon repro (do not put into DVC cache).",
|
||||
metavar="<filename>",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--always-changed",
|
||||
@@ -195,6 +217,18 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Always consider this DVC-file as changed.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--single-stage",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"--external",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Allow outputs that are outside of the DVC repository.",
|
||||
)
|
||||
run_parser.add_argument(
|
||||
"command", nargs=argparse.REMAINDER, help="Command to execute."
|
||||
)
|
||||
|
||||
+15
-9
@@ -2,7 +2,6 @@ import logging
|
||||
|
||||
from dvc.command.data_sync import CmdDataBase
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,7 +19,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
ind = indent * self.STATUS_INDENT
|
||||
|
||||
if isinstance(status, str):
|
||||
logger.info("{}{}".format(ind, status))
|
||||
logger.info(f"{ind}{status}")
|
||||
return
|
||||
|
||||
if isinstance(status, list):
|
||||
@@ -34,7 +33,7 @@ class CmdDataStatus(CmdDataBase):
|
||||
if isinstance(value, str):
|
||||
logger.info("{}{}{}".format(ind, self._normalize(value), key))
|
||||
elif value:
|
||||
logger.info("{}{}:".format(ind, key))
|
||||
logger.info(f"{ind}{key}:")
|
||||
self._show(value, indent + 1)
|
||||
|
||||
def run(self):
|
||||
@@ -47,17 +46,24 @@ class CmdDataStatus(CmdDataBase):
|
||||
remote=self.args.remote,
|
||||
all_branches=self.args.all_branches,
|
||||
all_tags=self.args.all_tags,
|
||||
all_commits=self.args.all_commits,
|
||||
with_deps=self.args.with_deps,
|
||||
recursive=self.args.recursive,
|
||||
)
|
||||
if st:
|
||||
if self.args.quiet:
|
||||
return 1
|
||||
else:
|
||||
self._show(st, indent)
|
||||
|
||||
if self.args.quiet:
|
||||
return bool(st)
|
||||
|
||||
if self.args.show_json:
|
||||
import json
|
||||
|
||||
logger.info(json.dumps(st))
|
||||
elif st:
|
||||
self._show(st, indent)
|
||||
else:
|
||||
logger.info(self.UP_TO_DATE_MSG)
|
||||
|
||||
except Exception:
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
logger.exception("failed to obtain data status")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command.base import fix_subparsers
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import to_yaml_string
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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.exception("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.exception("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(to_yaml_string(tags))
|
||||
except DvcException:
|
||||
logger.exception("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)
|
||||
@@ -1,11 +1,10 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -15,14 +14,17 @@ class CmdUnprotect(CmdBase):
|
||||
try:
|
||||
self.repo.unprotect(target)
|
||||
except DvcException:
|
||||
msg = "failed to unprotect '{}'".format(target)
|
||||
msg = f"failed to unprotect '{target}'"
|
||||
logger.exception(msg)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
UNPROTECT_HELP = "Unprotect data files or directories."
|
||||
UNPROTECT_HELP = (
|
||||
"Unprotect tracked files or directories (when hardlinks or symlinks "
|
||||
"have been enabled with `dvc config cache.type`)"
|
||||
)
|
||||
unprotect_parser = subparsers.add_parser(
|
||||
"unprotect",
|
||||
parents=[parent_parser],
|
||||
@@ -31,6 +33,6 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
unprotect_parser.add_argument(
|
||||
"targets", nargs="+", help="Data files/directories to unprotect."
|
||||
)
|
||||
"targets", nargs="+", help="Data files/directories to unprotect.",
|
||||
).complete = completion.FILE
|
||||
unprotect_parser.set_defaults(func=CmdUnprotect)
|
||||
|
||||
+25
-10
@@ -1,23 +1,25 @@
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from dvc.command.base import append_doc_link
|
||||
from dvc.command.base import CmdBase
|
||||
from dvc.command import completion
|
||||
from dvc.command.base import CmdBase, append_doc_link
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CmdUpdate(CmdBase):
|
||||
def run(self):
|
||||
ret = 0
|
||||
for target in self.args.targets:
|
||||
try:
|
||||
self.repo.update(target)
|
||||
except DvcException:
|
||||
logger.exception("failed to update '{}'.".format(target))
|
||||
ret = 1
|
||||
try:
|
||||
self.repo.update(
|
||||
targets=self.args.targets,
|
||||
rev=self.args.rev,
|
||||
recursive=self.args.recursive,
|
||||
)
|
||||
except DvcException:
|
||||
logger.exception("failed update data")
|
||||
ret = 1
|
||||
return ret
|
||||
|
||||
|
||||
@@ -31,6 +33,19 @@ def add_parser(subparsers, parent_parser):
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"targets", nargs="+", help="DVC-files to update."
|
||||
"targets", nargs="+", help="DVC-files to update.",
|
||||
).complete = completion.DVC_FILE
|
||||
update_parser.add_argument(
|
||||
"--rev",
|
||||
nargs="?",
|
||||
help="Git revision (e.g. SHA, branch, tag)",
|
||||
metavar="<commit>",
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"-R",
|
||||
"--recursive",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Update all stages in the specified directory.",
|
||||
)
|
||||
update_parser.set_defaults(func=CmdUpdate)
|
||||
|
||||
+55
-27
@@ -2,21 +2,23 @@ import argparse
|
||||
import itertools
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import uuid
|
||||
import pathlib
|
||||
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.scm.base import SCMError
|
||||
from dvc.system import System
|
||||
from dvc.utils import is_binary, relpath
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc.version import __version__
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
from dvc.utils import is_binary, relpath
|
||||
from dvc.command.base import CmdBaseNoRepo, append_doc_link
|
||||
from dvc.version import __version__
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.system import System
|
||||
from dvc.utils.pkg import PKG
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,11 +28,12 @@ class CmdVersion(CmdBaseNoRepo):
|
||||
from dvc.repo import Repo
|
||||
|
||||
info = [
|
||||
"DVC version: {}".format(__version__),
|
||||
"Python version: {}".format(platform.python_version()),
|
||||
"Platform: {}".format(platform.platform()),
|
||||
"Binary: {}".format(is_binary()),
|
||||
"Package: {}".format(PKG),
|
||||
f"DVC version: {__version__}",
|
||||
f"Python version: {platform.python_version()}",
|
||||
f"Platform: {platform.platform()}",
|
||||
f"Binary: {is_binary()}",
|
||||
f"Package: {PKG}",
|
||||
f"Supported remotes: {self.get_supported_remotes()}",
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -48,7 +51,7 @@ class CmdVersion(CmdBaseNoRepo):
|
||||
if psutil:
|
||||
fs_type = self.get_fs_type(repo.cache.local.cache_dir)
|
||||
info.append(
|
||||
"Filesystem type (cache directory): {}".format(fs_type)
|
||||
f"Filesystem type (cache directory): {fs_type}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -61,10 +64,15 @@ class CmdVersion(CmdBaseNoRepo):
|
||||
|
||||
except NotDvcRepoError:
|
||||
root_directory = os.getcwd()
|
||||
except SCMError:
|
||||
root_directory = os.getcwd()
|
||||
info.append("Repo: dvc, git (broken)")
|
||||
else:
|
||||
info.append("Repo: {}".format(_get_dvc_repo_info(repo)))
|
||||
|
||||
if psutil:
|
||||
fs_root = self.get_fs_type(os.path.abspath(root_directory))
|
||||
info.append("Filesystem type (workspace): {}".format(fs_root))
|
||||
info.append(f"Filesystem type (workspace): {fs_root}")
|
||||
|
||||
logger.info("\n".join(info))
|
||||
return 0
|
||||
@@ -86,9 +94,9 @@ class CmdVersion(CmdBaseNoRepo):
|
||||
@staticmethod
|
||||
def get_linktype_support_info(repo):
|
||||
links = {
|
||||
"reflink": System.reflink,
|
||||
"hardlink": System.hardlink,
|
||||
"symlink": System.symlink,
|
||||
"reflink": (System.reflink, None),
|
||||
"hardlink": (System.hardlink, System.is_hardlink),
|
||||
"symlink": (System.symlink, System.is_symlink),
|
||||
}
|
||||
|
||||
fname = "." + str(uuid.uuid4())
|
||||
@@ -98,26 +106,46 @@ class CmdVersion(CmdBaseNoRepo):
|
||||
|
||||
cache = []
|
||||
|
||||
for name, link in links.items():
|
||||
for name, (link, is_link) in links.items():
|
||||
try:
|
||||
link(src, dst)
|
||||
status = "supported"
|
||||
if is_link and not is_link(dst):
|
||||
status = "broken"
|
||||
os.unlink(dst)
|
||||
supported = True
|
||||
except DvcException:
|
||||
supported = False
|
||||
cache.append(
|
||||
"{name} - {supported}".format(
|
||||
name=name, supported=True if supported else False
|
||||
)
|
||||
)
|
||||
status = "not supported"
|
||||
cache.append(f"{name} - {status}")
|
||||
os.remove(src)
|
||||
|
||||
return ", ".join(cache)
|
||||
|
||||
@staticmethod
|
||||
def get_supported_remotes():
|
||||
from dvc.tree import TREES
|
||||
|
||||
supported_remotes = []
|
||||
for tree_cls in TREES:
|
||||
if not tree_cls.get_missing_deps():
|
||||
supported_remotes.append(tree_cls.scheme)
|
||||
|
||||
return ", ".join(supported_remotes)
|
||||
|
||||
|
||||
def _get_dvc_repo_info(repo):
|
||||
if repo.config.get("core", {}).get("no_scm", False):
|
||||
return "dvc (no_scm)"
|
||||
|
||||
if repo.root_dir != repo.scm.root_dir:
|
||||
return "dvc (subdir), git"
|
||||
|
||||
return "dvc, git"
|
||||
|
||||
|
||||
def add_parser(subparsers, parent_parser):
|
||||
VERSION_HELP = "Show DVC version and system/environment information."
|
||||
|
||||
VERSION_HELP = (
|
||||
"Display the DVC version and system/environment information."
|
||||
)
|
||||
version_parser = subparsers.add_parser(
|
||||
"version",
|
||||
parents=[parent_parser],
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""Python version compatibility code"""
|
||||
|
||||
# Backport os.fspath() from Python 3.6
|
||||
try:
|
||||
from os import fspath # noqa: F821
|
||||
|
||||
fspath_py35 = lambda s: s # noqa: E731
|
||||
except ImportError:
|
||||
|
||||
def fspath(path):
|
||||
"""Return the path representation of a path-like object.
|
||||
|
||||
If str or bytes is passed in, it is returned unchanged. Otherwise the
|
||||
os.PathLike interface is used to get the path representation. If the
|
||||
path representation is not str or bytes, TypeError is raised. If the
|
||||
provided path is not str, bytes, or os.PathLike, TypeError is raised.
|
||||
"""
|
||||
if isinstance(path, (str, bytes)):
|
||||
return path
|
||||
|
||||
# Work from the object's type to match method resolution of other magic
|
||||
# methods.
|
||||
path_type = type(path)
|
||||
try:
|
||||
path_repr = path_type.__fspath__(path)
|
||||
except AttributeError:
|
||||
if hasattr(path_type, "__fspath__"):
|
||||
raise
|
||||
else:
|
||||
raise TypeError(
|
||||
"expected str, bytes or os.PathLike object, "
|
||||
"not " + path_type.__name__
|
||||
)
|
||||
if isinstance(path_repr, (str, bytes)):
|
||||
return path_repr
|
||||
else:
|
||||
raise TypeError(
|
||||
"expected {}.__fspath__() to return str or bytes, "
|
||||
"not {}".format(path_type.__name__, type(path_repr).__name__)
|
||||
)
|
||||
|
||||
fspath_py35 = fspath
|
||||
+317
-407
@@ -1,17 +1,28 @@
|
||||
"""DVC config objects."""
|
||||
|
||||
import copy
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from functools import partial
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import configobj
|
||||
from voluptuous import Schema, Required, Optional, Invalid
|
||||
from voluptuous import All, Any, Lower, Range, Coerce, Match
|
||||
from funcy import cached_property, compact, re_find, walk_values
|
||||
from voluptuous import (
|
||||
ALLOW_EXTRA,
|
||||
All,
|
||||
Any,
|
||||
Coerce,
|
||||
Invalid,
|
||||
Lower,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.utils import relpath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,19 +31,11 @@ class ConfigError(DvcException):
|
||||
"""DVC config exception."""
|
||||
|
||||
def __init__(self, msg):
|
||||
super().__init__("config file error: {}".format(msg))
|
||||
super().__init__(f"config file error: {msg}")
|
||||
|
||||
|
||||
class NoRemoteError(ConfigError):
|
||||
def __init__(self, command):
|
||||
msg = (
|
||||
"no remote specified. Setup default remote with\n"
|
||||
" dvc remote default <name>\n"
|
||||
"or use:\n"
|
||||
" dvc {} -r <name>\n".format(command)
|
||||
)
|
||||
|
||||
super().__init__(msg)
|
||||
pass
|
||||
|
||||
|
||||
def supported_cache_type(types):
|
||||
@@ -56,13 +59,12 @@ def supported_cache_type(types):
|
||||
|
||||
|
||||
# Checks that value is either true or false and converts it to bool
|
||||
Bool = All(
|
||||
to_bool = Bool = All(
|
||||
Lower,
|
||||
Any("true", "false"),
|
||||
lambda v: v == "true",
|
||||
msg="expected true or false",
|
||||
)
|
||||
to_bool = Schema(Bool)
|
||||
|
||||
|
||||
def Choices(*choices):
|
||||
@@ -75,8 +77,139 @@ def Choices(*choices):
|
||||
return Any(*choices, msg="expected one of {}".format(", ".join(choices)))
|
||||
|
||||
|
||||
class Config(object): # pylint: disable=too-many-instance-attributes
|
||||
"""Class that manages configuration files for a dvc repo.
|
||||
def ByUrl(mapping):
|
||||
schemas = walk_values(Schema, mapping)
|
||||
|
||||
def validate(data):
|
||||
if "url" not in data:
|
||||
raise Invalid("expected 'url'")
|
||||
|
||||
parsed = urlparse(data["url"])
|
||||
# Windows absolute paths should really have scheme == "" (local)
|
||||
if os.name == "nt" and len(parsed.scheme) == 1 and parsed.netloc == "":
|
||||
return schemas[""](data)
|
||||
if parsed.scheme not in schemas:
|
||||
raise Invalid(f"Unsupported URL type {parsed.scheme}://")
|
||||
|
||||
return schemas[parsed.scheme](data)
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
class RelPath(str):
|
||||
pass
|
||||
|
||||
|
||||
REMOTE_COMMON = {
|
||||
"url": str,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
Optional("no_traverse"): Bool, # obsoleted
|
||||
"verify": Bool,
|
||||
}
|
||||
LOCAL_COMMON = {
|
||||
"type": supported_cache_type,
|
||||
Optional("protected", default=False): Bool, # obsoleted
|
||||
"shared": All(Lower, Choices("group")),
|
||||
Optional("slow_link_warning", default=True): Bool,
|
||||
}
|
||||
HTTP_COMMON = {
|
||||
"auth": All(Lower, Choices("basic", "digest", "custom")),
|
||||
"custom_auth_header": str,
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
}
|
||||
SCHEMA = {
|
||||
"core": {
|
||||
"remote": Lower,
|
||||
"checksum_jobs": All(Coerce(int), Range(1)),
|
||||
Optional("interactive", default=False): Bool,
|
||||
Optional("analytics", default=True): Bool,
|
||||
Optional("hardlink_lock", default=False): Bool,
|
||||
Optional("no_scm", default=False): Bool,
|
||||
},
|
||||
"cache": {
|
||||
"local": str,
|
||||
"s3": str,
|
||||
"gs": str,
|
||||
"hdfs": str,
|
||||
"ssh": str,
|
||||
"azure": str,
|
||||
# This is for default local cache
|
||||
"dir": str,
|
||||
**LOCAL_COMMON,
|
||||
},
|
||||
"remote": {
|
||||
str: ByUrl(
|
||||
{
|
||||
"": {**LOCAL_COMMON, **REMOTE_COMMON},
|
||||
"s3": {
|
||||
"region": str,
|
||||
"profile": str,
|
||||
"credentialpath": str,
|
||||
"endpointurl": str,
|
||||
Optional("listobjects", default=False): Bool,
|
||||
Optional("use_ssl", default=True): Bool,
|
||||
"sse": str,
|
||||
"sse_kms_key_id": str,
|
||||
"acl": str,
|
||||
"grant_read": str,
|
||||
"grant_read_acp": str,
|
||||
"grant_write_acp": str,
|
||||
"grant_full_control": str,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gs": {
|
||||
"projectname": str,
|
||||
"credentialpath": str,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"ssh": {
|
||||
"type": supported_cache_type,
|
||||
"port": Coerce(int),
|
||||
"user": str,
|
||||
"password": str,
|
||||
"ask_password": Bool,
|
||||
"keyfile": str,
|
||||
"timeout": Coerce(int),
|
||||
"gss_auth": Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"hdfs": {"user": str, **REMOTE_COMMON},
|
||||
"azure": {"connection_string": str, **REMOTE_COMMON},
|
||||
"oss": {
|
||||
"oss_key_id": str,
|
||||
"oss_key_secret": str,
|
||||
"oss_endpoint": str,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"gdrive": {
|
||||
"gdrive_use_service_account": Bool,
|
||||
"gdrive_client_id": str,
|
||||
"gdrive_client_secret": str,
|
||||
"gdrive_user_credentials_file": str,
|
||||
"gdrive_service_account_email": str,
|
||||
"gdrive_service_account_user_email": str,
|
||||
"gdrive_service_account_p12_file_path": str,
|
||||
Optional("gdrive_trash_only", default=False): Bool,
|
||||
**REMOTE_COMMON,
|
||||
},
|
||||
"http": {**HTTP_COMMON, **REMOTE_COMMON},
|
||||
"https": {**HTTP_COMMON, **REMOTE_COMMON},
|
||||
"remote": {str: object}, # Any of the above options are valid
|
||||
}
|
||||
)
|
||||
},
|
||||
"state": {
|
||||
"row_limit": All(Coerce(int), Range(1)),
|
||||
"row_cleanup_quota": All(Coerce(int), Range(0, 100)),
|
||||
},
|
||||
}
|
||||
COMPILED_SCHEMA = Schema(SCHEMA)
|
||||
|
||||
|
||||
class Config(dict):
|
||||
"""Class that manages configuration files for a DVC repo.
|
||||
|
||||
Args:
|
||||
dvc_dir (str): optional path to `.dvc` directory, that is used to
|
||||
@@ -85,172 +218,25 @@ class Config(object): # pylint: disable=too-many-instance-attributes
|
||||
validate (bool): optional flag to tell dvc if it should validate the
|
||||
config or just load it as is. 'True' by default.
|
||||
|
||||
|
||||
Raises:
|
||||
ConfigError: thrown when config has an invalid format.
|
||||
ConfigError: thrown if config has an invalid format.
|
||||
"""
|
||||
|
||||
APPNAME = "dvc"
|
||||
APPAUTHOR = "iterative"
|
||||
|
||||
# NOTE: used internally in RemoteLOCAL to know config
|
||||
# location, that url should resolved relative to.
|
||||
PRIVATE_CWD = "_cwd"
|
||||
# In the order they shadow each other
|
||||
LEVELS = ("system", "global", "repo", "local")
|
||||
|
||||
CONFIG = "config"
|
||||
CONFIG_LOCAL = "config.local"
|
||||
|
||||
CREDENTIALPATH = "credentialpath"
|
||||
def __init__(
|
||||
self, dvc_dir=None, validate=True, tree=None,
|
||||
): # pylint: disable=super-init-not-called
|
||||
from dvc.tree.local import LocalRemoteTree
|
||||
|
||||
LEVEL_LOCAL = 0
|
||||
LEVEL_REPO = 1
|
||||
LEVEL_GLOBAL = 2
|
||||
LEVEL_SYSTEM = 3
|
||||
|
||||
SECTION_CORE = "core"
|
||||
SECTION_CORE_LOGLEVEL = "loglevel"
|
||||
SECTION_CORE_LOGLEVEL_SCHEMA = All(
|
||||
Lower, Choices("info", "debug", "warning", "error")
|
||||
)
|
||||
SECTION_CORE_REMOTE = "remote"
|
||||
SECTION_CORE_INTERACTIVE = "interactive"
|
||||
SECTION_CORE_ANALYTICS = "analytics"
|
||||
SECTION_CORE_CHECKSUM_JOBS = "checksum_jobs"
|
||||
SECTION_CORE_HARDLINK_LOCK = "hardlink_lock"
|
||||
|
||||
SECTION_CACHE = "cache"
|
||||
SECTION_CACHE_DIR = "dir"
|
||||
SECTION_CACHE_TYPE = "type"
|
||||
SECTION_CACHE_PROTECTED = "protected"
|
||||
SECTION_CACHE_SHARED = "shared"
|
||||
SECTION_CACHE_SHARED_SCHEMA = All(Lower, Choices("group"))
|
||||
SECTION_CACHE_LOCAL = "local"
|
||||
SECTION_CACHE_S3 = "s3"
|
||||
SECTION_CACHE_GS = "gs"
|
||||
SECTION_CACHE_SSH = "ssh"
|
||||
SECTION_CACHE_HDFS = "hdfs"
|
||||
SECTION_CACHE_AZURE = "azure"
|
||||
SECTION_CACHE_SLOW_LINK_WARNING = "slow_link_warning"
|
||||
SECTION_CACHE_SCHEMA = {
|
||||
SECTION_CACHE_LOCAL: str,
|
||||
SECTION_CACHE_S3: str,
|
||||
SECTION_CACHE_GS: str,
|
||||
SECTION_CACHE_HDFS: str,
|
||||
SECTION_CACHE_SSH: str,
|
||||
SECTION_CACHE_AZURE: str,
|
||||
SECTION_CACHE_DIR: str,
|
||||
SECTION_CACHE_TYPE: supported_cache_type,
|
||||
Optional(SECTION_CACHE_PROTECTED, default=False): Bool,
|
||||
SECTION_CACHE_SHARED: SECTION_CACHE_SHARED_SCHEMA,
|
||||
PRIVATE_CWD: str,
|
||||
Optional(SECTION_CACHE_SLOW_LINK_WARNING, default=True): Bool,
|
||||
}
|
||||
|
||||
SECTION_CORE_SCHEMA = {
|
||||
SECTION_CORE_LOGLEVEL: SECTION_CORE_LOGLEVEL_SCHEMA,
|
||||
SECTION_CORE_REMOTE: Lower,
|
||||
Optional(SECTION_CORE_INTERACTIVE, default=False): Bool,
|
||||
Optional(SECTION_CORE_ANALYTICS, default=True): Bool,
|
||||
SECTION_CORE_CHECKSUM_JOBS: All(Coerce(int), Range(1)),
|
||||
Optional(SECTION_CORE_HARDLINK_LOCK, default=False): Bool,
|
||||
}
|
||||
|
||||
# aws specific options
|
||||
SECTION_AWS_CREDENTIALPATH = CREDENTIALPATH
|
||||
SECTION_AWS_ENDPOINT_URL = "endpointurl"
|
||||
SECTION_AWS_LIST_OBJECTS = "listobjects"
|
||||
SECTION_AWS_REGION = "region"
|
||||
SECTION_AWS_PROFILE = "profile"
|
||||
SECTION_AWS_USE_SSL = "use_ssl"
|
||||
SECTION_AWS_SSE = "sse"
|
||||
SECTION_AWS_ACL = "acl"
|
||||
SECTION_AWS_GRANT_READ = "grant_read"
|
||||
SECTION_AWS_GRANT_READ_ACP = "grant_read_acp"
|
||||
SECTION_AWS_GRANT_WRITE_ACP = "grant_write_acp"
|
||||
SECTION_AWS_GRANT_FULL_CONTROL = "grant_full_control"
|
||||
|
||||
# gcp specific options
|
||||
SECTION_GCP_CREDENTIALPATH = CREDENTIALPATH
|
||||
SECTION_GCP_PROJECTNAME = "projectname"
|
||||
|
||||
# azure specific option
|
||||
SECTION_AZURE_CONNECTION_STRING = "connection_string"
|
||||
|
||||
# Alibabacloud oss options
|
||||
SECTION_OSS_ACCESS_KEY_ID = "oss_key_id"
|
||||
SECTION_OSS_ACCESS_KEY_SECRET = "oss_key_secret"
|
||||
SECTION_OSS_ENDPOINT = "oss_endpoint"
|
||||
|
||||
# GDrive options
|
||||
SECTION_GDRIVE_CLIENT_ID = "gdrive_client_id"
|
||||
SECTION_GDRIVE_CLIENT_SECRET = "gdrive_client_secret"
|
||||
SECTION_GDRIVE_USER_CREDENTIALS_FILE = "gdrive_user_credentials_file"
|
||||
|
||||
SECTION_REMOTE_REGEX = r'^\s*remote\s*"(?P<name>.*)"\s*$'
|
||||
SECTION_REMOTE_FMT = 'remote "{}"'
|
||||
SECTION_REMOTE_URL = "url"
|
||||
SECTION_REMOTE_USER = "user"
|
||||
SECTION_REMOTE_PORT = "port"
|
||||
SECTION_REMOTE_KEY_FILE = "keyfile"
|
||||
SECTION_REMOTE_TIMEOUT = "timeout"
|
||||
SECTION_REMOTE_PASSWORD = "password"
|
||||
SECTION_REMOTE_ASK_PASSWORD = "ask_password"
|
||||
SECTION_REMOTE_GSS_AUTH = "gss_auth"
|
||||
SECTION_REMOTE_NO_TRAVERSE = "no_traverse"
|
||||
SECTION_REMOTE_SCHEMA = {
|
||||
Required(SECTION_REMOTE_URL): str,
|
||||
SECTION_AWS_REGION: str,
|
||||
SECTION_AWS_PROFILE: str,
|
||||
SECTION_AWS_CREDENTIALPATH: str,
|
||||
SECTION_AWS_ENDPOINT_URL: str,
|
||||
Optional(SECTION_AWS_LIST_OBJECTS, default=False): Bool,
|
||||
Optional(SECTION_AWS_USE_SSL, default=True): Bool,
|
||||
SECTION_AWS_SSE: str,
|
||||
SECTION_AWS_ACL: str,
|
||||
SECTION_AWS_GRANT_READ: str,
|
||||
SECTION_AWS_GRANT_READ_ACP: str,
|
||||
SECTION_AWS_GRANT_WRITE_ACP: str,
|
||||
SECTION_AWS_GRANT_FULL_CONTROL: str,
|
||||
SECTION_GCP_PROJECTNAME: str,
|
||||
SECTION_CACHE_TYPE: supported_cache_type,
|
||||
Optional(SECTION_CACHE_PROTECTED, default=False): Bool,
|
||||
SECTION_REMOTE_USER: str,
|
||||
SECTION_REMOTE_PORT: Coerce(int),
|
||||
SECTION_REMOTE_KEY_FILE: str,
|
||||
SECTION_REMOTE_TIMEOUT: Coerce(int),
|
||||
SECTION_REMOTE_PASSWORD: str,
|
||||
SECTION_REMOTE_ASK_PASSWORD: Bool,
|
||||
SECTION_REMOTE_GSS_AUTH: Bool,
|
||||
SECTION_AZURE_CONNECTION_STRING: str,
|
||||
SECTION_OSS_ACCESS_KEY_ID: str,
|
||||
SECTION_OSS_ACCESS_KEY_SECRET: str,
|
||||
SECTION_OSS_ENDPOINT: str,
|
||||
SECTION_GDRIVE_CLIENT_ID: str,
|
||||
SECTION_GDRIVE_CLIENT_SECRET: str,
|
||||
SECTION_GDRIVE_USER_CREDENTIALS_FILE: str,
|
||||
PRIVATE_CWD: str,
|
||||
Optional(SECTION_REMOTE_NO_TRAVERSE, default=True): Bool,
|
||||
}
|
||||
|
||||
SECTION_STATE = "state"
|
||||
SECTION_STATE_ROW_LIMIT = "row_limit"
|
||||
SECTION_STATE_ROW_CLEANUP_QUOTA = "row_cleanup_quota"
|
||||
SECTION_STATE_SCHEMA = {
|
||||
SECTION_STATE_ROW_LIMIT: All(Coerce(int), Range(1)),
|
||||
SECTION_STATE_ROW_CLEANUP_QUOTA: All(Coerce(int), Range(0, 100)),
|
||||
}
|
||||
|
||||
SCHEMA = {
|
||||
Optional(SECTION_CORE, default={}): SECTION_CORE_SCHEMA,
|
||||
Match(SECTION_REMOTE_REGEX): SECTION_REMOTE_SCHEMA,
|
||||
Optional(SECTION_CACHE, default={}): SECTION_CACHE_SCHEMA,
|
||||
Optional(SECTION_STATE, default={}): SECTION_STATE_SCHEMA,
|
||||
}
|
||||
COMPILED_SCHEMA = Schema(SCHEMA)
|
||||
|
||||
def __init__(self, dvc_dir=None, validate=True):
|
||||
self.dvc_dir = dvc_dir
|
||||
self.validate = validate
|
||||
|
||||
if not dvc_dir:
|
||||
try:
|
||||
@@ -262,33 +248,34 @@ class Config(object): # pylint: disable=too-many-instance-attributes
|
||||
else:
|
||||
self.dvc_dir = os.path.abspath(os.path.realpath(dvc_dir))
|
||||
|
||||
self.load()
|
||||
self.wtree = LocalRemoteTree(None, {"url": self.dvc_dir})
|
||||
self.tree = tree or self.wtree
|
||||
|
||||
@staticmethod
|
||||
def get_global_config_dir():
|
||||
"""Returns global config location. E.g. ~/.config/dvc/config.
|
||||
self.load(validate=validate)
|
||||
|
||||
Returns:
|
||||
str: path to the global config directory.
|
||||
"""
|
||||
from appdirs import user_config_dir
|
||||
@classmethod
|
||||
def get_dir(cls, level):
|
||||
from appdirs import user_config_dir, site_config_dir
|
||||
|
||||
return user_config_dir(
|
||||
appname=Config.APPNAME, appauthor=Config.APPAUTHOR
|
||||
)
|
||||
assert level in ("global", "system")
|
||||
|
||||
@staticmethod
|
||||
def get_system_config_dir():
|
||||
"""Returns system config location. E.g. /etc/dvc.conf.
|
||||
if level == "global":
|
||||
return user_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
if level == "system":
|
||||
return site_config_dir(cls.APPNAME, cls.APPAUTHOR)
|
||||
|
||||
Returns:
|
||||
str: path to the system config directory.
|
||||
"""
|
||||
from appdirs import site_config_dir
|
||||
@cached_property
|
||||
def files(self):
|
||||
files = {
|
||||
level: os.path.join(self.get_dir(level), self.CONFIG)
|
||||
for level in ("system", "global")
|
||||
}
|
||||
|
||||
return site_config_dir(
|
||||
appname=Config.APPNAME, appauthor=Config.APPAUTHOR
|
||||
)
|
||||
if self.dvc_dir is not None:
|
||||
files["repo"] = os.path.join(self.dvc_dir, self.CONFIG)
|
||||
files["local"] = os.path.join(self.dvc_dir, self.CONFIG_LOCAL)
|
||||
|
||||
return files
|
||||
|
||||
@staticmethod
|
||||
def init(dvc_dir):
|
||||
@@ -304,243 +291,166 @@ class Config(object): # pylint: disable=too-many-instance-attributes
|
||||
open(config_file, "w+").close()
|
||||
return Config(dvc_dir)
|
||||
|
||||
def _resolve_cache_path(self, config):
|
||||
cache = config.get(self.SECTION_CACHE)
|
||||
if cache is None:
|
||||
return
|
||||
|
||||
cache_dir = cache.get(self.SECTION_CACHE_DIR)
|
||||
if cache_dir is None:
|
||||
return
|
||||
|
||||
cache[self.PRIVATE_CWD] = os.path.dirname(config.filename)
|
||||
|
||||
def _resolve_paths(self, config):
|
||||
if config.filename is None:
|
||||
return config
|
||||
|
||||
ret = copy.deepcopy(config)
|
||||
self._resolve_cache_path(ret)
|
||||
|
||||
for section in ret.values():
|
||||
if self.SECTION_REMOTE_URL not in section.keys():
|
||||
continue
|
||||
|
||||
section[self.PRIVATE_CWD] = os.path.dirname(ret.filename)
|
||||
|
||||
return ret
|
||||
|
||||
def _load_configs(self):
|
||||
system_config_file = os.path.join(
|
||||
self.get_system_config_dir(), self.CONFIG
|
||||
)
|
||||
|
||||
global_config_file = os.path.join(
|
||||
self.get_global_config_dir(), self.CONFIG
|
||||
)
|
||||
|
||||
self._system_config = configobj.ConfigObj(system_config_file)
|
||||
self._global_config = configobj.ConfigObj(global_config_file)
|
||||
self._repo_config = configobj.ConfigObj()
|
||||
self._local_config = configobj.ConfigObj()
|
||||
|
||||
if not self.dvc_dir:
|
||||
return
|
||||
|
||||
config_file = os.path.join(self.dvc_dir, self.CONFIG)
|
||||
config_local_file = os.path.join(self.dvc_dir, self.CONFIG_LOCAL)
|
||||
|
||||
self._repo_config = configobj.ConfigObj(config_file)
|
||||
self._local_config = configobj.ConfigObj(config_local_file)
|
||||
|
||||
@property
|
||||
def config_local_file(self):
|
||||
return self._local_config.filename
|
||||
|
||||
@property
|
||||
def config_file(self):
|
||||
return self._repo_config.filename
|
||||
|
||||
def load(self):
|
||||
def load(self, validate=True):
|
||||
"""Loads config from all the config files.
|
||||
|
||||
Raises:
|
||||
dvc.config.ConfigError: thrown if config has invalid format.
|
||||
ConfigError: thrown if config has an invalid format.
|
||||
"""
|
||||
self._load_configs()
|
||||
conf = self.load_config_to_level()
|
||||
|
||||
self.config = configobj.ConfigObj()
|
||||
for c in [
|
||||
self._system_config,
|
||||
self._global_config,
|
||||
self._repo_config,
|
||||
self._local_config,
|
||||
]:
|
||||
c = self._resolve_paths(c)
|
||||
c = self._lower(c)
|
||||
self.config.merge(c)
|
||||
if validate:
|
||||
conf = self.validate(conf)
|
||||
|
||||
if not self.validate:
|
||||
return
|
||||
self.clear()
|
||||
self.update(conf)
|
||||
|
||||
d = self.config.dict()
|
||||
try:
|
||||
d = self.COMPILED_SCHEMA(d)
|
||||
except Invalid as exc:
|
||||
raise ConfigError(str(exc)) from exc
|
||||
self.config = configobj.ConfigObj(d, write_empty_values=True)
|
||||
# Add resolved default cache.dir
|
||||
if not self["cache"].get("dir") and self.dvc_dir:
|
||||
self["cache"]["dir"] = os.path.join(self.dvc_dir, "cache")
|
||||
|
||||
def save(self, config=None):
|
||||
"""Saves config to config files.
|
||||
def _load_config(self, level):
|
||||
filename = self.files[level]
|
||||
tree = self.tree if level == "repo" else self.wtree
|
||||
|
||||
Raises:
|
||||
dvc.config.ConfigError: thrown if failed to write config file.
|
||||
"""
|
||||
if config is not None:
|
||||
clist = [config]
|
||||
if tree.exists(filename):
|
||||
with tree.open(filename) as fobj:
|
||||
conf_obj = configobj.ConfigObj(fobj)
|
||||
else:
|
||||
clist = [
|
||||
self._system_config,
|
||||
self._global_config,
|
||||
self._repo_config,
|
||||
self._local_config,
|
||||
]
|
||||
conf_obj = configobj.ConfigObj()
|
||||
return _parse_remotes(_lower_keys(conf_obj.dict()))
|
||||
|
||||
for conf in clist:
|
||||
self._save(conf)
|
||||
def _save_config(self, level, conf_dict):
|
||||
filename = self.files[level]
|
||||
tree = self.tree if level == "repo" else self.wtree
|
||||
|
||||
logger.debug(f"Writing '{filename}'.")
|
||||
|
||||
tree.makedirs(os.path.dirname(filename))
|
||||
|
||||
config = configobj.ConfigObj(_pack_remotes(conf_dict))
|
||||
with tree.open(filename, "wb") as fobj:
|
||||
config.write(fobj)
|
||||
config.filename = filename
|
||||
|
||||
def load_one(self, level):
|
||||
conf = self._load_config(level)
|
||||
conf = self._load_paths(conf, self.files[level])
|
||||
|
||||
# Auto-verify sections
|
||||
for key in COMPILED_SCHEMA.schema:
|
||||
conf.setdefault(key, {})
|
||||
|
||||
return conf
|
||||
|
||||
@staticmethod
|
||||
def _load_paths(conf, filename):
|
||||
abs_conf_dir = os.path.abspath(os.path.dirname(filename))
|
||||
|
||||
def resolve(path):
|
||||
if os.path.isabs(path) or re.match(r"\w+://", path):
|
||||
return path
|
||||
return RelPath(os.path.join(abs_conf_dir, path))
|
||||
|
||||
return Config._map_dirs(conf, resolve)
|
||||
|
||||
@staticmethod
|
||||
def _to_relpath(conf_dir, path):
|
||||
if re.match(r"\w+://", path):
|
||||
return path
|
||||
|
||||
if isinstance(path, RelPath) or not os.path.isabs(path):
|
||||
path = relpath(path, conf_dir)
|
||||
|
||||
return PathInfo(path).as_posix()
|
||||
|
||||
@staticmethod
|
||||
def _save_paths(conf, filename):
|
||||
conf_dir = os.path.dirname(filename)
|
||||
rel = partial(Config._to_relpath, conf_dir)
|
||||
|
||||
return Config._map_dirs(conf, rel)
|
||||
|
||||
@staticmethod
|
||||
def _map_dirs(conf, func):
|
||||
dirs_schema = {
|
||||
"cache": {"dir": func},
|
||||
"remote": {
|
||||
str: {"url": func, "gdrive_user_credentials_file": func}
|
||||
},
|
||||
}
|
||||
return Schema(dirs_schema, extra=ALLOW_EXTRA)(conf)
|
||||
|
||||
def load_config_to_level(self, level=None):
|
||||
merged_conf = {}
|
||||
for merge_level in self.LEVELS:
|
||||
if merge_level == level:
|
||||
break
|
||||
if merge_level in self.files:
|
||||
_merge(merged_conf, self.load_one(merge_level))
|
||||
return merged_conf
|
||||
|
||||
@contextmanager
|
||||
def edit(self, level="repo"):
|
||||
if level in {"repo", "local"} and self.dvc_dir is None:
|
||||
raise ConfigError("Not inside a DVC repo")
|
||||
|
||||
conf = self.load_one(level)
|
||||
yield conf
|
||||
|
||||
conf = self._save_paths(conf, self.files[level])
|
||||
|
||||
merged_conf = self.load_config_to_level(level)
|
||||
_merge(merged_conf, conf)
|
||||
self.validate(merged_conf)
|
||||
|
||||
self._save_config(level, conf)
|
||||
self.load()
|
||||
|
||||
@staticmethod
|
||||
def _save(config):
|
||||
if config.filename is None:
|
||||
return
|
||||
|
||||
logger.debug("Writing '{}'.".format(config.filename))
|
||||
dname = os.path.dirname(os.path.abspath(config.filename))
|
||||
def validate(data):
|
||||
try:
|
||||
os.makedirs(dname)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.EEXIST:
|
||||
raise
|
||||
config.write()
|
||||
return COMPILED_SCHEMA(data)
|
||||
except Invalid as exc:
|
||||
raise ConfigError(str(exc)) from None
|
||||
|
||||
def unset(self, section, opt=None, level=None, force=False):
|
||||
"""Unsets specified option and/or section in the config.
|
||||
|
||||
Args:
|
||||
section (str): section name.
|
||||
opt (str): optional option name.
|
||||
level (int): config level to use.
|
||||
force (bool): don't error-out even if section doesn't exist. False
|
||||
by default.
|
||||
def _parse_remotes(conf):
|
||||
result = {"remote": {}}
|
||||
|
||||
Raises:
|
||||
dvc.config.ConfigError: thrown if section doesn't exist and
|
||||
`force != True`.
|
||||
"""
|
||||
config = self.get_configobj(level)
|
||||
|
||||
if section not in config.keys():
|
||||
if force:
|
||||
return
|
||||
raise ConfigError("section '{}' doesn't exist".format(section))
|
||||
|
||||
if opt:
|
||||
if opt not in config[section].keys():
|
||||
if force:
|
||||
return
|
||||
raise ConfigError(
|
||||
"option '{}.{}' doesn't exist".format(section, opt)
|
||||
)
|
||||
del config[section][opt]
|
||||
|
||||
if not config[section]:
|
||||
del config[section]
|
||||
for section, val in conf.items():
|
||||
name = re_find(r'^\s*remote\s*"(.*)"\s*$', section)
|
||||
if name:
|
||||
result["remote"][name] = val
|
||||
else:
|
||||
del config[section]
|
||||
result[section] = val
|
||||
|
||||
self.save(config)
|
||||
return result
|
||||
|
||||
def set(self, section, opt, value, level=None, force=True):
|
||||
"""Sets specified option in the config.
|
||||
|
||||
Args:
|
||||
section (str): section name.
|
||||
opt (str): option name.
|
||||
value: value to set option to.
|
||||
level (int): config level to use.
|
||||
force (bool): set option even if section already exists. True by
|
||||
default.
|
||||
def _pack_remotes(conf):
|
||||
# Drop empty sections
|
||||
result = compact(conf)
|
||||
|
||||
Raises:
|
||||
dvc.config.ConfigError: thrown if section already exists and
|
||||
`force != True`.
|
||||
# Transform remote.name -> 'remote "name"'
|
||||
for name, val in conf["remote"].items():
|
||||
result[f'remote "{name}"'] = val
|
||||
result.pop("remote", None)
|
||||
|
||||
"""
|
||||
config = self.get_configobj(level)
|
||||
return result
|
||||
|
||||
if section not in config.keys():
|
||||
config[section] = {}
|
||||
elif not force:
|
||||
raise ConfigError(
|
||||
"Section '{}' already exists. Use `-f|--force` to overwrite "
|
||||
"section with new value.".format(section)
|
||||
)
|
||||
|
||||
config[section][opt] = value
|
||||
self.save(config)
|
||||
def _merge(into, update):
|
||||
"""Merges second dict into first recursively"""
|
||||
for key, val in update.items():
|
||||
if isinstance(into.get(key), dict) and isinstance(val, dict):
|
||||
_merge(into[key], val)
|
||||
else:
|
||||
into[key] = val
|
||||
|
||||
def get(self, section, opt=None, level=None):
|
||||
"""Return option value from the config.
|
||||
|
||||
Args:
|
||||
section (str): section name.
|
||||
opt (str): option name.
|
||||
level (int): config level to use.
|
||||
|
||||
Returns:
|
||||
value (str, int): option value.
|
||||
"""
|
||||
config = self.get_configobj(level)
|
||||
|
||||
if section not in config.keys():
|
||||
raise ConfigError("section '{}' doesn't exist".format(section))
|
||||
|
||||
if opt not in config[section].keys():
|
||||
raise ConfigError(
|
||||
"option '{}.{}' doesn't exist".format(section, opt)
|
||||
)
|
||||
|
||||
return config[section][opt]
|
||||
|
||||
@staticmethod
|
||||
def _lower(config):
|
||||
new_config = configobj.ConfigObj()
|
||||
for s_key, s_value in config.items():
|
||||
new_s = {}
|
||||
for key, value in s_value.items():
|
||||
new_s[key.lower()] = str(value)
|
||||
new_config[s_key.lower()] = new_s
|
||||
return new_config
|
||||
|
||||
def get_configobj(self, level):
|
||||
configs = {
|
||||
self.LEVEL_LOCAL: self._local_config,
|
||||
self.LEVEL_REPO: self._repo_config,
|
||||
self.LEVEL_GLOBAL: self._global_config,
|
||||
self.LEVEL_SYSTEM: self._system_config,
|
||||
}
|
||||
|
||||
return configs.get(level, self._repo_config)
|
||||
|
||||
def list_options(self, section_regex, option, level=None):
|
||||
ret = {}
|
||||
config = self.get_configobj(level)
|
||||
for section in config.keys():
|
||||
r = re.match(section_regex, section)
|
||||
if r:
|
||||
name = r.group("name")
|
||||
value = config[section].get(option, "")
|
||||
ret[name] = value
|
||||
return ret
|
||||
def _lower_keys(data):
|
||||
return {
|
||||
k.lower(): _lower_keys(v) if isinstance(v, dict) else v
|
||||
for k, v in data.items()
|
||||
}
|
||||
|
||||
+3
-6
@@ -7,9 +7,7 @@ import sys
|
||||
from subprocess import Popen
|
||||
|
||||
from dvc.env import DVC_DAEMON
|
||||
from dvc.utils import fix_env
|
||||
from dvc.utils import is_binary
|
||||
|
||||
from dvc.utils import fix_env, is_binary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,7 +46,6 @@ def _spawn_posix(cmd, env):
|
||||
os._exit(1) # pylint: disable=protected-access
|
||||
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
@@ -68,7 +65,7 @@ def _spawn_posix(cmd, env):
|
||||
|
||||
|
||||
def _spawn(cmd, env):
|
||||
logger.debug("Trying to spawn '{}'".format(cmd))
|
||||
logger.debug(f"Trying to spawn '{cmd}'")
|
||||
|
||||
if os.name == "nt":
|
||||
_spawn_windows(cmd, env)
|
||||
@@ -77,7 +74,7 @@ def _spawn(cmd, env):
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
logger.debug("Spawned '{}'".format(cmd))
|
||||
logger.debug(f"Spawned '{cmd}'")
|
||||
|
||||
|
||||
def daemon(args):
|
||||
|
||||
+17
-19
@@ -6,15 +6,12 @@ import os
|
||||
import pydoc
|
||||
import sys
|
||||
|
||||
from grandalf.graphs import Edge
|
||||
from grandalf.graphs import Graph
|
||||
from grandalf.graphs import Vertex
|
||||
from grandalf.graphs import Edge, Graph, Vertex
|
||||
from grandalf.layouts import SugiyamaLayout
|
||||
from grandalf.routing import EdgeViewer
|
||||
from grandalf.routing import route_with_lines
|
||||
from grandalf.routing import EdgeViewer, route_with_lines
|
||||
|
||||
from dvc.env import DVC_PAGER
|
||||
|
||||
from dvc.utils import format_link
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,17 +37,19 @@ def find_pager():
|
||||
if env_pager:
|
||||
return make_pager(env_pager)
|
||||
|
||||
if os.system("({}) 2>{}".format(DEFAULT_PAGER, os.devnull)) == 0:
|
||||
if os.system(f"({DEFAULT_PAGER}) 2>{os.devnull}") == 0:
|
||||
return make_pager(DEFAULT_PAGER_FORMATTED)
|
||||
|
||||
logger.warning(
|
||||
"Unable to find `less` in the PATH. Check out "
|
||||
"man.dvc.org/doc/command-reference/pipeline/show for more info."
|
||||
"{} for more info.".format(
|
||||
format_link("https://man.dvc.org/pipeline/show")
|
||||
)
|
||||
)
|
||||
return pydoc.plainpager
|
||||
|
||||
|
||||
class VertexViewer(object):
|
||||
class VertexViewer:
|
||||
"""Class to define vertex box boundaries that will be accounted for during
|
||||
graph building by grandalf.
|
||||
|
||||
@@ -76,7 +75,7 @@ class VertexViewer(object):
|
||||
return self._w
|
||||
|
||||
|
||||
class AsciiCanvas(object):
|
||||
class AsciiCanvas:
|
||||
"""Class for drawing in ASCII.
|
||||
|
||||
Args:
|
||||
@@ -93,14 +92,13 @@ class AsciiCanvas(object):
|
||||
self.cols = cols
|
||||
self.lines = lines
|
||||
|
||||
self.canvas = [[" "] * cols for l in range(lines)]
|
||||
self.canvas = [[" "] * cols for line in range(lines)]
|
||||
|
||||
def draw(self):
|
||||
"""Draws ASCII canvas on the screen."""
|
||||
pager = find_pager()
|
||||
lines = map("".join, self.canvas)
|
||||
joined_lines = os.linesep.join(lines)
|
||||
pager(joined_lines)
|
||||
return joined_lines
|
||||
|
||||
def point(self, x, y, char):
|
||||
"""Create a point on ASCII canvas.
|
||||
@@ -146,21 +144,21 @@ class AsciiCanvas(object):
|
||||
if dx == 0:
|
||||
y = y0
|
||||
else:
|
||||
y = y0 + int(round((x - x0) * dy / float((dx))))
|
||||
y = y0 + int(round((x - x0) * dy / float(dx)))
|
||||
self.point(x, y, char)
|
||||
elif y0 < y1:
|
||||
for y in range(y0, y1 + 1):
|
||||
if dy == 0:
|
||||
x = x0
|
||||
else:
|
||||
x = x0 + int(round((y - y0) * dx / float((dy))))
|
||||
x = x0 + int(round((y - y0) * dx / float(dy)))
|
||||
self.point(x, y, char)
|
||||
else:
|
||||
for y in range(y1, y0 + 1):
|
||||
if dy == 0:
|
||||
x = x0
|
||||
else:
|
||||
x = x1 + int(round((y - y1) * dx / float((dy))))
|
||||
x = x1 + int(round((y - y1) * dx / float(dy)))
|
||||
self.point(x, y, char)
|
||||
|
||||
def text(self, x, y, text):
|
||||
@@ -214,7 +212,7 @@ def _build_sugiyama_layout(vertexes, edges):
|
||||
# Y
|
||||
#
|
||||
|
||||
vertexes = {v: Vertex(" {} ".format(v)) for v in vertexes}
|
||||
vertexes = {v: Vertex(f" {v} ") for v in vertexes}
|
||||
# NOTE: reverting edges to correctly orientate the graph
|
||||
edges = [Edge(vertexes[e], vertexes[s]) for s, e in edges]
|
||||
vertexes = vertexes.values()
|
||||
@@ -224,7 +222,7 @@ def _build_sugiyama_layout(vertexes, edges):
|
||||
vertex.view = VertexViewer(vertex.data)
|
||||
|
||||
# NOTE: determine min box length to create the best layout
|
||||
minw = min([v.view.w for v in vertexes])
|
||||
minw = min(v.view.w for v in vertexes)
|
||||
|
||||
for edge in edges:
|
||||
edge.view = EdgeViewer()
|
||||
@@ -317,4 +315,4 @@ def draw(vertexes, edges):
|
||||
int(round(x - minx)) + 1, int(round(y - miny)) + 1, vertex.data
|
||||
)
|
||||
|
||||
canvas.draw()
|
||||
return canvas.draw()
|
||||
|
||||
+53
-33
@@ -2,15 +2,13 @@
|
||||
|
||||
import logging
|
||||
|
||||
from dvc.config import Config
|
||||
from dvc.config import NoRemoteError
|
||||
from dvc.remote import Remote
|
||||
|
||||
from dvc.remote import get_remote
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataCloud(object):
|
||||
class DataCloud:
|
||||
"""Class that manages dvc remotes.
|
||||
|
||||
Args:
|
||||
@@ -24,69 +22,91 @@ class DataCloud(object):
|
||||
def __init__(self, repo):
|
||||
self.repo = repo
|
||||
|
||||
@property
|
||||
def _config(self):
|
||||
return self.repo.config.config
|
||||
def get_remote(self, name=None, command="<command>"):
|
||||
if not name:
|
||||
name = self.repo.config["core"].get("remote")
|
||||
|
||||
@property
|
||||
def _core(self):
|
||||
return self._config.get(Config.SECTION_CORE, {})
|
||||
if name:
|
||||
return self._init_remote(name)
|
||||
|
||||
def get_remote(self, remote=None, command="<command>"):
|
||||
if not remote:
|
||||
remote = self._core.get(Config.SECTION_CORE_REMOTE)
|
||||
if bool(self.repo.config["remote"]):
|
||||
error_msg = (
|
||||
"no remote specified. Setup default remote with\n"
|
||||
" dvc remote default <remote name>\n"
|
||||
"or use:\n"
|
||||
" dvc {} -r <remote name>".format(command)
|
||||
)
|
||||
else:
|
||||
error_msg = (
|
||||
"no remote specified. Create a default remote with\n"
|
||||
" dvc remote add -d <remote name> <remote url>"
|
||||
)
|
||||
|
||||
if remote:
|
||||
return self._init_remote(remote)
|
||||
raise NoRemoteError(error_msg)
|
||||
|
||||
raise NoRemoteError(command)
|
||||
def _init_remote(self, name):
|
||||
return get_remote(self.repo, name=name)
|
||||
|
||||
def _init_remote(self, remote):
|
||||
return Remote(self.repo, name=remote)
|
||||
|
||||
def push(self, cache, jobs=None, remote=None, show_checksums=False):
|
||||
def push(
|
||||
self, cache, jobs=None, remote=None, show_checksums=False,
|
||||
):
|
||||
"""Push data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
cache (NamedCache): named checksums to push to the cloud.
|
||||
jobs (int): number of jobs that can be running simultaneously.
|
||||
remote (dvc.remote.base.RemoteBASE): optional remote to push to.
|
||||
remote (dvc.remote.base.BaseRemote): optional remote to push to.
|
||||
By default remote from core.remote config option is used.
|
||||
show_checksums (bool): show checksums instead of file names in
|
||||
information messages.
|
||||
"""
|
||||
remote = self.get_remote(remote, "push")
|
||||
|
||||
return self.repo.cache.local.push(
|
||||
cache,
|
||||
jobs=jobs,
|
||||
remote=self.get_remote(remote, "push"),
|
||||
show_checksums=show_checksums,
|
||||
cache, jobs=jobs, remote=remote, show_checksums=show_checksums,
|
||||
)
|
||||
|
||||
def pull(self, cache, jobs=None, remote=None, show_checksums=False):
|
||||
def pull(
|
||||
self, cache, jobs=None, remote=None, show_checksums=False,
|
||||
):
|
||||
"""Pull data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
cache (NamedCache): named checksums to pull from the cloud.
|
||||
jobs (int): number of jobs that can be running simultaneously.
|
||||
remote (dvc.remote.base.RemoteBASE): optional remote to pull from.
|
||||
remote (dvc.remote.base.BaseRemote): optional remote to pull from.
|
||||
By default remote from core.remote config option is used.
|
||||
show_checksums (bool): show checksums instead of file names in
|
||||
information messages.
|
||||
"""
|
||||
return self.repo.cache.local.pull(
|
||||
cache,
|
||||
jobs=jobs,
|
||||
remote=self.get_remote(remote, "pull"),
|
||||
show_checksums=show_checksums,
|
||||
remote = self.get_remote(remote, "pull")
|
||||
|
||||
downloaded_items_num = self.repo.cache.local.pull(
|
||||
cache, jobs=jobs, remote=remote, show_checksums=show_checksums
|
||||
)
|
||||
|
||||
if not remote.tree.verify:
|
||||
self._save_pulled_checksums(cache)
|
||||
|
||||
return downloaded_items_num
|
||||
|
||||
def _save_pulled_checksums(self, cache):
|
||||
for checksum in cache.scheme_keys("local"):
|
||||
cache_file = self.repo.cache.local.tree.hash_to_path_info(checksum)
|
||||
if self.repo.cache.local.tree.exists(cache_file):
|
||||
# We can safely save here, as existing corrupted files will
|
||||
# be removed upon status, while files corrupted during
|
||||
# download will not be moved from tmp_file
|
||||
# (see `RemoteBASE.download()`)
|
||||
self.repo.state.save(cache_file, checksum)
|
||||
|
||||
def status(self, cache, jobs=None, remote=None, show_checksums=False):
|
||||
"""Check status of data items in a cloud-agnostic way.
|
||||
|
||||
Args:
|
||||
cache (NamedCache): named checksums to check status for.
|
||||
jobs (int): number of jobs that can be running simultaneously.
|
||||
remote (dvc.remote.base.RemoteBASE): optional remote to compare
|
||||
remote (dvc.remote.base.BaseRemote): optional remote to compare
|
||||
cache to. By default remote from core.remote config option
|
||||
is used.
|
||||
show_checksums (bool): show checksums instead of file names in
|
||||
|
||||
+76
-41
@@ -1,37 +1,42 @@
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import dvc.output as output
|
||||
from dvc.dependency.gs import DependencyGS
|
||||
from dvc.dependency.hdfs import DependencyHDFS
|
||||
from dvc.dependency.http import DependencyHTTP
|
||||
from dvc.dependency.https import DependencyHTTPS
|
||||
from dvc.dependency.local import DependencyLOCAL
|
||||
from dvc.dependency.s3 import DependencyS3
|
||||
from dvc.dependency.ssh import DependencySSH
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.remote import Remote
|
||||
from dvc.dependency.azure import AzureDependency
|
||||
from dvc.dependency.gs import GSDependency
|
||||
from dvc.dependency.hdfs import HDFSDependency
|
||||
from dvc.dependency.http import HTTPDependency
|
||||
from dvc.dependency.https import HTTPSDependency
|
||||
from dvc.dependency.local import LocalDependency
|
||||
from dvc.dependency.param import ParamsDependency
|
||||
from dvc.dependency.s3 import S3Dependency
|
||||
from dvc.dependency.ssh import SSHDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
from dvc.scheme import Schemes
|
||||
from .repo import DependencyREPO
|
||||
|
||||
from ..tree import get_cloud_tree
|
||||
from .repo import RepoDependency
|
||||
|
||||
DEPS = [
|
||||
DependencyGS,
|
||||
DependencyHDFS,
|
||||
DependencyHTTP,
|
||||
DependencyHTTPS,
|
||||
DependencyS3,
|
||||
DependencySSH,
|
||||
# NOTE: DependencyLOCAL is the default choice
|
||||
AzureDependency,
|
||||
GSDependency,
|
||||
HDFSDependency,
|
||||
HTTPDependency,
|
||||
HTTPSDependency,
|
||||
S3Dependency,
|
||||
SSHDependency,
|
||||
# NOTE: LocalDependency is the default choice
|
||||
]
|
||||
|
||||
DEP_MAP = {
|
||||
Schemes.LOCAL: DependencyLOCAL,
|
||||
Schemes.SSH: DependencySSH,
|
||||
Schemes.S3: DependencyS3,
|
||||
Schemes.GS: DependencyGS,
|
||||
Schemes.HDFS: DependencyHDFS,
|
||||
Schemes.HTTP: DependencyHTTP,
|
||||
Schemes.HTTPS: DependencyHTTPS,
|
||||
Schemes.LOCAL: LocalDependency,
|
||||
Schemes.SSH: SSHDependency,
|
||||
Schemes.S3: S3Dependency,
|
||||
Schemes.AZURE: AzureDependency,
|
||||
Schemes.GS: GSDependency,
|
||||
Schemes.HDFS: HDFSDependency,
|
||||
Schemes.HTTP: HTTPDependency,
|
||||
Schemes.HTTPS: HTTPSDependency,
|
||||
}
|
||||
|
||||
|
||||
@@ -40,39 +45,69 @@ DEP_MAP = {
|
||||
# cached, see -o and -O flags for `dvc run`) and 'metric' (whether or not
|
||||
# output is a metric file and how to parse it, see `-M` flag for `dvc run`).
|
||||
SCHEMA = output.SCHEMA.copy()
|
||||
del SCHEMA[OutputBase.PARAM_CACHE]
|
||||
del SCHEMA[OutputBase.PARAM_METRIC]
|
||||
SCHEMA[DependencyREPO.PARAM_REPO] = DependencyREPO.REPO_SCHEMA
|
||||
del SCHEMA[BaseOutput.PARAM_CACHE]
|
||||
del SCHEMA[BaseOutput.PARAM_METRIC]
|
||||
SCHEMA.update(RepoDependency.REPO_SCHEMA)
|
||||
SCHEMA.update(ParamsDependency.PARAM_SCHEMA)
|
||||
|
||||
|
||||
def _get(stage, p, info):
|
||||
parsed = urlparse(p)
|
||||
parsed = urlparse(p) if p else None
|
||||
if parsed and parsed.scheme == "remote":
|
||||
tree = get_cloud_tree(stage.repo, name=parsed.netloc)
|
||||
return DEP_MAP[tree.scheme](stage, p, info, tree=tree)
|
||||
|
||||
if parsed.scheme == "remote":
|
||||
remote = Remote(stage.repo, name=parsed.netloc)
|
||||
return DEP_MAP[remote.scheme](stage, p, info, remote=remote)
|
||||
if info and info.get(RepoDependency.PARAM_REPO):
|
||||
repo = info.pop(RepoDependency.PARAM_REPO)
|
||||
return RepoDependency(repo, stage, p, info)
|
||||
|
||||
if info and info.get(DependencyREPO.PARAM_REPO):
|
||||
repo = info.pop(DependencyREPO.PARAM_REPO)
|
||||
return DependencyREPO(repo, stage, p, info)
|
||||
if info and info.get(ParamsDependency.PARAM_PARAMS):
|
||||
params = info.pop(ParamsDependency.PARAM_PARAMS)
|
||||
return ParamsDependency(stage, p, params)
|
||||
|
||||
for d in DEPS:
|
||||
if d.supported(p):
|
||||
return d(stage, p, info)
|
||||
return DependencyLOCAL(stage, p, info)
|
||||
return LocalDependency(stage, p, info)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
ret = []
|
||||
for d in d_list:
|
||||
p = d.pop(OutputBase.PARAM_PATH)
|
||||
p = d.pop(BaseOutput.PARAM_PATH, None)
|
||||
ret.append(_get(stage, p, d))
|
||||
return ret
|
||||
|
||||
|
||||
def loads_from(stage, s_list, erepo=None):
|
||||
ret = []
|
||||
for s in s_list:
|
||||
info = {DependencyREPO.PARAM_REPO: erepo} if erepo else {}
|
||||
ret.append(_get(stage, s, info))
|
||||
return ret
|
||||
assert isinstance(s_list, list)
|
||||
info = {RepoDependency.PARAM_REPO: erepo} if erepo else {}
|
||||
return [_get(stage, s, info.copy()) for s in s_list]
|
||||
|
||||
|
||||
def _merge_params(s_list):
|
||||
d = defaultdict(list)
|
||||
default_file = ParamsDependency.DEFAULT_PARAMS_FILE
|
||||
for key in s_list:
|
||||
if isinstance(key, str):
|
||||
d[default_file].append(key)
|
||||
continue
|
||||
if not isinstance(key, dict):
|
||||
msg = "Only list of str/dict is supported. Got: "
|
||||
msg += f"'{type(key).__name__}'."
|
||||
raise ValueError(msg)
|
||||
|
||||
for k, params in key.items():
|
||||
if not isinstance(params, list):
|
||||
msg = "Expected list of params for custom params file "
|
||||
msg += f"'{k}', got '{type(params).__name__}'."
|
||||
raise ValueError(msg)
|
||||
d[k].extend(params)
|
||||
return d
|
||||
|
||||
|
||||
def loads_params(stage, s_list):
|
||||
d = _merge_params(s_list)
|
||||
return [
|
||||
ParamsDependency(stage, path, params) for path, params in d.items()
|
||||
]
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..tree.azure import AzureRemoteTree
|
||||
|
||||
|
||||
class AzureDependency(BaseDependency, BaseOutput):
|
||||
TREE_CLS = AzureRemoteTree
|
||||
@@ -3,29 +3,27 @@ from dvc.exceptions import DvcException
|
||||
|
||||
class DependencyDoesNotExistError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = "dependency '{}' does not exist".format(path)
|
||||
msg = f"dependency '{path}' does not exist"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class DependencyIsNotFileOrDirError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = "dependency '{}' is not a file or directory".format(path)
|
||||
msg = f"dependency '{path}' is not a file or directory"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class DependencyIsStageFileError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(
|
||||
"Stage file '{}' cannot be a dependency.".format(path)
|
||||
)
|
||||
super().__init__(f"Stage file '{path}' cannot be a dependency.")
|
||||
|
||||
|
||||
class DependencyBase(object):
|
||||
class BaseDependency:
|
||||
IS_DEPENDENCY = True
|
||||
|
||||
DoesNotExistError = DependencyDoesNotExistError
|
||||
IsNotFileOrDirError = DependencyIsNotFileOrDirError
|
||||
IsStageFileError = DependencyIsStageFileError
|
||||
|
||||
def update(self):
|
||||
def update(self, rev=None):
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.dependency.base import DependencyBase
|
||||
from dvc.output.gs import OutputGS
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.gs import GSOutput
|
||||
|
||||
|
||||
class DependencyGS(DependencyBase, OutputGS):
|
||||
class GSDependency(BaseDependency, GSOutput):
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.dependency.base import DependencyBase
|
||||
from dvc.output.hdfs import OutputHDFS
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.hdfs import HDFSOutput
|
||||
|
||||
|
||||
class DependencyHDFS(DependencyBase, OutputHDFS):
|
||||
class HDFSDependency(BaseDependency, HDFSOutput):
|
||||
pass
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from dvc.dependency.base import DependencyBase
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.remote.http import RemoteHTTP
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..tree.http import HTTPRemoteTree
|
||||
|
||||
|
||||
class DependencyHTTP(DependencyBase, OutputBase):
|
||||
REMOTE = RemoteHTTP
|
||||
class HTTPDependency(BaseDependency, BaseOutput):
|
||||
TREE_CLS = HTTPRemoteTree
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from .http import DependencyHTTP
|
||||
from dvc.remote.https import RemoteHTTPS
|
||||
from ..tree.https import HTTPSRemoteTree
|
||||
from .http import HTTPDependency
|
||||
|
||||
|
||||
class DependencyHTTPS(DependencyHTTP):
|
||||
REMOTE = RemoteHTTPS
|
||||
class HTTPSDependency(HTTPDependency):
|
||||
TREE_CLS = HTTPSRemoteTree
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.dependency.base import DependencyBase
|
||||
from dvc.output.local import OutputLOCAL
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.local import LocalOutput
|
||||
|
||||
|
||||
class DependencyLOCAL(DependencyBase, OutputLOCAL):
|
||||
class LocalDependency(BaseDependency, LocalOutput):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
import dpath.util
|
||||
import yaml
|
||||
from voluptuous import Any
|
||||
|
||||
from dvc.dependency.local import LocalDependency
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class MissingParamsError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class BadParamFileError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class ParamsDependency(LocalDependency):
|
||||
PARAM_PARAMS = "params"
|
||||
PARAM_SCHEMA = {PARAM_PARAMS: Any(dict, list, None)}
|
||||
DEFAULT_PARAMS_FILE = "params.yaml"
|
||||
|
||||
def __init__(self, stage, path, params):
|
||||
info = {}
|
||||
self.params = []
|
||||
if params:
|
||||
if isinstance(params, list):
|
||||
self.params = params
|
||||
else:
|
||||
assert isinstance(params, dict)
|
||||
self.params = list(params.keys())
|
||||
info = params
|
||||
|
||||
super().__init__(
|
||||
stage,
|
||||
path
|
||||
or os.path.join(stage.repo.root_dir, self.DEFAULT_PARAMS_FILE),
|
||||
info=info,
|
||||
)
|
||||
|
||||
def fill_values(self, values=None):
|
||||
"""Load params values dynamically."""
|
||||
if not values:
|
||||
return
|
||||
for param in self.params:
|
||||
if param in values:
|
||||
self.info[param] = values[param]
|
||||
|
||||
def save(self):
|
||||
super().save()
|
||||
self.info = self.save_info()
|
||||
|
||||
def status(self):
|
||||
status = super().status()
|
||||
|
||||
if status[str(self)] == "deleted":
|
||||
return status
|
||||
|
||||
status = defaultdict(dict)
|
||||
info = self.read_params()
|
||||
for param in self.params:
|
||||
if param not in info.keys():
|
||||
st = "deleted"
|
||||
elif param not in self.info:
|
||||
st = "new"
|
||||
elif info[param] != self.info[param]:
|
||||
st = "modified"
|
||||
else:
|
||||
assert info[param] == self.info[param]
|
||||
continue
|
||||
|
||||
status[str(self)][param] = st
|
||||
|
||||
return status
|
||||
|
||||
def dumpd(self):
|
||||
return {
|
||||
self.PARAM_PATH: self.def_path,
|
||||
self.PARAM_PARAMS: self.info or self.params,
|
||||
}
|
||||
|
||||
def read_params(self):
|
||||
if not self.exists:
|
||||
return {}
|
||||
|
||||
with self.repo.tree.open(self.path_info, "r") as fobj:
|
||||
try:
|
||||
config = yaml.safe_load(fobj)
|
||||
except yaml.YAMLError as exc:
|
||||
raise BadParamFileError(
|
||||
f"Unable to read parameters from '{self}'"
|
||||
) from exc
|
||||
|
||||
ret = {}
|
||||
for param in self.params:
|
||||
try:
|
||||
ret[param] = dpath.util.get(config, param, separator=".")
|
||||
except KeyError:
|
||||
pass
|
||||
return ret
|
||||
|
||||
def save_info(self):
|
||||
info = self.read_params()
|
||||
|
||||
missing_params = set(self.params) - set(info.keys())
|
||||
if missing_params:
|
||||
raise MissingParamsError(
|
||||
"Parameters '{}' are missing from '{}'.".format(
|
||||
", ".join(missing_params), self,
|
||||
)
|
||||
)
|
||||
|
||||
return info
|
||||
+61
-80
@@ -1,31 +1,32 @@
|
||||
import copy
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
|
||||
from funcy import merge
|
||||
from voluptuous import Required
|
||||
|
||||
from .local import DependencyLOCAL
|
||||
from dvc.external_repo import cached_clone
|
||||
from dvc.external_repo import external_repo
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.exceptions import OutputNotFoundError
|
||||
from dvc.exceptions import PathMissingError
|
||||
from dvc.utils.fs import fs_copy
|
||||
from dvc.path_info import PathInfo
|
||||
|
||||
from .local import LocalDependency
|
||||
|
||||
|
||||
class DependencyREPO(DependencyLOCAL):
|
||||
class RepoDependency(LocalDependency):
|
||||
PARAM_REPO = "repo"
|
||||
PARAM_URL = "url"
|
||||
PARAM_REV = "rev"
|
||||
PARAM_REV_LOCK = "rev_lock"
|
||||
|
||||
REPO_SCHEMA = {PARAM_URL: str, PARAM_REV: str, PARAM_REV_LOCK: str}
|
||||
REPO_SCHEMA = {
|
||||
PARAM_REPO: {
|
||||
Required(PARAM_URL): str,
|
||||
PARAM_REV: str,
|
||||
PARAM_REV_LOCK: str,
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, def_repo, stage, *args, **kwargs):
|
||||
self.def_repo = def_repo
|
||||
super().__init__(stage, *args, **kwargs)
|
||||
|
||||
def _parse_path(self, remote, path):
|
||||
def _parse_path(self, tree, path):
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -35,24 +36,42 @@ class DependencyREPO(DependencyLOCAL):
|
||||
@property
|
||||
def repo_pair(self):
|
||||
d = self.def_repo
|
||||
return d[self.PARAM_URL], d[self.PARAM_REV_LOCK] or d[self.PARAM_REV]
|
||||
rev = d.get(self.PARAM_REV_LOCK) or d.get(self.PARAM_REV)
|
||||
return d[self.PARAM_URL], rev
|
||||
|
||||
def __str__(self):
|
||||
return "{} ({})".format(self.def_path, self.def_repo[self.PARAM_URL])
|
||||
|
||||
@contextmanager
|
||||
def _make_repo(self, **overrides):
|
||||
with external_repo(**merge(self.def_repo, overrides)) as repo:
|
||||
yield repo
|
||||
def _make_repo(self, *, locked=True):
|
||||
from dvc.external_repo import external_repo
|
||||
|
||||
d = self.def_repo
|
||||
rev = (d.get("rev_lock") if locked else None) or d.get("rev")
|
||||
return external_repo(d["url"], rev=rev)
|
||||
|
||||
def _get_checksum(self, locked=True):
|
||||
from dvc.repo.tree import RepoTree
|
||||
|
||||
with self._make_repo(locked=locked) as repo:
|
||||
try:
|
||||
return repo.find_out_by_relpath(self.def_path).info["md5"]
|
||||
except OutputNotFoundError:
|
||||
path = PathInfo(os.path.join(repo.root_dir, self.def_path))
|
||||
|
||||
# we want stream but not fetch, so DVC out directories are
|
||||
# walked, but dir contents is not fetched
|
||||
tree = RepoTree(repo, stream=True)
|
||||
|
||||
# We are polluting our repo cache with some dir listing here
|
||||
if tree.isdir(path):
|
||||
return self.repo.cache.local.tree.get_hash(path, tree=tree)
|
||||
return tree.get_file_hash(path)
|
||||
|
||||
def status(self):
|
||||
with self._make_repo() as repo:
|
||||
current = repo.find_out_by_relpath(self.def_path).info
|
||||
current_checksum = self._get_checksum(locked=True)
|
||||
updated_checksum = self._get_checksum(locked=False)
|
||||
|
||||
with self._make_repo(rev_lock=None) as repo:
|
||||
updated = repo.find_out_by_relpath(self.def_path).info
|
||||
|
||||
if current != updated:
|
||||
if current_checksum != updated_checksum:
|
||||
return {str(self): "update available"}
|
||||
|
||||
return {}
|
||||
@@ -63,63 +82,25 @@ class DependencyREPO(DependencyLOCAL):
|
||||
def dumpd(self):
|
||||
return {self.PARAM_PATH: self.def_path, self.PARAM_REPO: self.def_repo}
|
||||
|
||||
def fetch(self):
|
||||
with self._make_repo(
|
||||
cache_dir=self.repo.cache.local.cache_dir
|
||||
) as repo:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.scm.get_rev()
|
||||
|
||||
out = repo.find_out_by_relpath(self.def_path)
|
||||
with repo.state:
|
||||
repo.cloud.pull(out.get_used_cache())
|
||||
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _is_git_file(repo_dir, path):
|
||||
from dvc.repo import Repo
|
||||
|
||||
if os.path.isabs(path):
|
||||
return False
|
||||
|
||||
try:
|
||||
repo = Repo(repo_dir)
|
||||
except NotDvcRepoError:
|
||||
return True
|
||||
|
||||
try:
|
||||
output = repo.find_out_by_relpath(path)
|
||||
return not output.use_cache
|
||||
except OutputNotFoundError:
|
||||
return True
|
||||
finally:
|
||||
repo.close()
|
||||
|
||||
def _copy_if_git_file(self, to_path):
|
||||
src_path = self.def_path
|
||||
repo_dir = cached_clone(**self.def_repo)
|
||||
|
||||
if not self._is_git_file(repo_dir, src_path):
|
||||
return False
|
||||
|
||||
src_full_path = os.path.join(repo_dir, src_path)
|
||||
dst_full_path = os.path.abspath(to_path)
|
||||
fs_copy(src_full_path, dst_full_path)
|
||||
return True
|
||||
|
||||
def download(self, to):
|
||||
try:
|
||||
if self._copy_if_git_file(to.fspath):
|
||||
return
|
||||
with self._make_repo() as repo:
|
||||
if self.def_repo.get(self.PARAM_REV_LOCK) is None:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
|
||||
|
||||
out = self.fetch()
|
||||
to.info = copy.copy(out.info)
|
||||
to.checkout()
|
||||
except (FileNotFoundError):
|
||||
raise PathMissingError(
|
||||
self.def_path, self.def_repo[self.PARAM_URL]
|
||||
)
|
||||
cache = self.repo.cache.local
|
||||
with repo.use_cache(cache):
|
||||
_, _, cache_infos = repo.fetch_external([self.def_path])
|
||||
cache.checkout(to.path_info, cache_infos[0])
|
||||
|
||||
def update(self):
|
||||
with self._make_repo(rev_lock=None) as repo:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.scm.get_rev()
|
||||
def update(self, rev=None):
|
||||
if rev:
|
||||
self.def_repo[self.PARAM_REV] = rev
|
||||
|
||||
with self._make_repo(locked=False) as repo:
|
||||
self.def_repo[self.PARAM_REV_LOCK] = repo.get_rev()
|
||||
|
||||
def changed_checksum(self):
|
||||
# From current repo point of view what describes RepoDependency is its
|
||||
# origin project url and rev_lock, and it makes RepoDependency
|
||||
# immutable, hence its impossible for checksum to change.
|
||||
return False
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.dependency.base import DependencyBase
|
||||
from dvc.output.s3 import OutputS3
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.s3 import S3Output
|
||||
|
||||
|
||||
class DependencyS3(DependencyBase, OutputS3):
|
||||
class S3Dependency(BaseDependency, S3Output):
|
||||
pass
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from dvc.dependency.base import DependencyBase
|
||||
from dvc.output.ssh import OutputSSH
|
||||
from dvc.dependency.base import BaseDependency
|
||||
from dvc.output.ssh import SSHOutput
|
||||
|
||||
|
||||
class DependencySSH(DependencyBase, OutputSSH):
|
||||
class SSHDependency(BaseDependency, SSHOutput):
|
||||
pass
|
||||
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
import collections
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
from voluptuous import MultipleInvalid
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.stage import serialize
|
||||
from dvc.stage.exceptions import (
|
||||
StageFileBadNameError,
|
||||
StageFileDoesNotExistError,
|
||||
StageFileFormatError,
|
||||
StageFileIsNotDvcFileError,
|
||||
)
|
||||
from dvc.stage.loader import SingleStageLoader, StageLoader
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils.collections import apply_diff
|
||||
from dvc.utils.yaml import dump_yaml, parse_yaml, parse_yaml_for_update
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DVC_FILE = "Dvcfile"
|
||||
DVC_FILE_SUFFIX = ".dvc"
|
||||
PIPELINE_FILE = "dvc.yaml"
|
||||
PIPELINE_LOCK = "dvc.lock"
|
||||
|
||||
|
||||
class LockfileCorruptedError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
def is_valid_filename(path):
|
||||
return path.endswith(DVC_FILE_SUFFIX) or os.path.basename(path) in [
|
||||
DVC_FILE,
|
||||
PIPELINE_FILE,
|
||||
]
|
||||
|
||||
|
||||
def is_dvc_file(path):
|
||||
return os.path.isfile(path) and (
|
||||
is_valid_filename(path) or os.path.basename(path) == PIPELINE_LOCK
|
||||
)
|
||||
|
||||
|
||||
def check_dvc_filename(path):
|
||||
if not is_valid_filename(path):
|
||||
raise StageFileBadNameError(
|
||||
"bad DVC-file name '{}'. DVC-files should be named "
|
||||
"'Dvcfile' or have a '.dvc' suffix (e.g. '{}.dvc').".format(
|
||||
relpath(path), os.path.basename(path)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class FileMixin:
|
||||
SCHEMA = None
|
||||
|
||||
def __init__(self, repo, path, **kwargs):
|
||||
self.repo = repo
|
||||
self.path = path
|
||||
|
||||
def __repr__(self):
|
||||
return "{}: {}".format(
|
||||
self.__class__.__name__, relpath(self.path, self.repo.root_dir)
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.path)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.repo == other.repo and os.path.abspath(
|
||||
self.path
|
||||
) == os.path.abspath(other.path)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}: {self.relpath}"
|
||||
|
||||
@property
|
||||
def relpath(self):
|
||||
return relpath(self.path)
|
||||
|
||||
def exists(self):
|
||||
return self.repo.tree.exists(self.path)
|
||||
|
||||
def _load(self):
|
||||
# it raises the proper exceptions by priority:
|
||||
# 1. when the file doesn't exists
|
||||
# 2. filename is not a DVC-file
|
||||
# 3. path doesn't represent a regular file
|
||||
if not self.exists():
|
||||
raise StageFileDoesNotExistError(self.path)
|
||||
check_dvc_filename(self.path)
|
||||
if not self.repo.tree.isfile(self.path):
|
||||
raise StageFileIsNotDvcFileError(self.path)
|
||||
|
||||
with self.repo.tree.open(self.path) as fd:
|
||||
stage_text = fd.read()
|
||||
d = parse_yaml(stage_text, self.path)
|
||||
self.validate(d, self.relpath)
|
||||
return d, stage_text
|
||||
|
||||
@classmethod
|
||||
def validate(cls, d, fname=None):
|
||||
assert isinstance(cls.SCHEMA, collections.abc.Callable)
|
||||
try:
|
||||
cls.SCHEMA(d) # pylint: disable=not-callable
|
||||
except MultipleInvalid as exc:
|
||||
raise StageFileFormatError(f"'{fname}' format error: {exc}")
|
||||
|
||||
def remove(self, force=False): # pylint: disable=unused-argument
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
os.unlink(self.path)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SingleStageFile(FileMixin):
|
||||
from dvc.schema import COMPILED_SINGLE_STAGE_SCHEMA as SCHEMA
|
||||
|
||||
@property
|
||||
def stage(self):
|
||||
data, raw = self._load()
|
||||
return SingleStageLoader.load_stage(self, data, raw)
|
||||
|
||||
@property
|
||||
def stages(self):
|
||||
data, raw = self._load()
|
||||
return SingleStageLoader(self, data, raw)
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
"""Dumps given stage appropriately in the dvcfile."""
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
assert not isinstance(stage, PipelineStage)
|
||||
check_dvc_filename(self.path)
|
||||
logger.debug(
|
||||
"Saving information to '{file}'.".format(file=relpath(self.path))
|
||||
)
|
||||
dump_yaml(self.path, serialize.to_single_stage_file(stage))
|
||||
self.repo.scm.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage): # pylint: disable=unused-argument
|
||||
self.remove()
|
||||
|
||||
|
||||
class PipelineFile(FileMixin):
|
||||
"""Abstraction for pipelines file, .yaml + .lock combined."""
|
||||
|
||||
from dvc.schema import COMPILED_MULTI_STAGE_SCHEMA as SCHEMA
|
||||
|
||||
@property
|
||||
def _lockfile(self):
|
||||
return Lockfile(self.repo, os.path.splitext(self.path)[0] + ".lock")
|
||||
|
||||
def dump(
|
||||
self, stage, update_pipeline=False, no_lock=False, **kwargs
|
||||
): # pylint: disable=arguments-differ
|
||||
"""Dumps given stage appropriately in the dvcfile."""
|
||||
from dvc.stage import PipelineStage
|
||||
|
||||
assert isinstance(stage, PipelineStage)
|
||||
check_dvc_filename(self.path)
|
||||
|
||||
if update_pipeline and not stage.is_data_source:
|
||||
self._dump_pipeline_file(stage)
|
||||
|
||||
if not no_lock:
|
||||
self._dump_lockfile(stage)
|
||||
|
||||
def _dump_lockfile(self, stage):
|
||||
self._lockfile.dump(stage)
|
||||
|
||||
def _dump_pipeline_file(self, stage):
|
||||
data = {}
|
||||
if self.exists():
|
||||
with open(self.path) as fd:
|
||||
data = parse_yaml_for_update(fd.read(), self.path)
|
||||
else:
|
||||
logger.info("Creating '%s'", self.relpath)
|
||||
open(self.path, "w+").close()
|
||||
|
||||
data["stages"] = data.get("stages", {})
|
||||
stage_data = serialize.to_pipeline_file(stage)
|
||||
existing_entry = stage.name in data["stages"]
|
||||
|
||||
action = "Modifying" if existing_entry else "Adding"
|
||||
logger.info("%s stage '%s' in '%s'", action, stage.name, self.relpath)
|
||||
|
||||
if existing_entry:
|
||||
orig_stage_data = data["stages"][stage.name]
|
||||
if "meta" in orig_stage_data:
|
||||
stage_data[stage.name]["meta"] = orig_stage_data["meta"]
|
||||
apply_diff(stage_data[stage.name], orig_stage_data)
|
||||
else:
|
||||
data["stages"].update(stage_data)
|
||||
|
||||
dump_yaml(self.path, data)
|
||||
self.repo.scm.track_file(self.relpath)
|
||||
|
||||
@property
|
||||
def stage(self):
|
||||
raise DvcException(
|
||||
"PipelineFile has multiple stages. Please specify it's name."
|
||||
)
|
||||
|
||||
@property
|
||||
def stages(self):
|
||||
data, _ = self._load()
|
||||
lockfile_data = self._lockfile.load()
|
||||
return StageLoader(self, data.get("stages", {}), lockfile_data)
|
||||
|
||||
def remove(self, force=False):
|
||||
if not force:
|
||||
logger.warning("Cannot remove pipeline file.")
|
||||
return
|
||||
|
||||
super().remove()
|
||||
self._lockfile.remove()
|
||||
|
||||
def remove_stage(self, stage):
|
||||
self._lockfile.remove_stage(stage)
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
with open(self.path, "r") as f:
|
||||
d = parse_yaml_for_update(f.read(), self.path)
|
||||
|
||||
self.validate(d, self.path)
|
||||
if stage.name not in d.get("stages", {}):
|
||||
return
|
||||
|
||||
logger.debug("Removing '%s' from '%s'", stage.name, self.path)
|
||||
del d["stages"][stage.name]
|
||||
|
||||
if d["stages"]:
|
||||
dump_yaml(self.path, d)
|
||||
else:
|
||||
super().remove()
|
||||
|
||||
|
||||
class Lockfile(FileMixin):
|
||||
from dvc.schema import COMPILED_LOCKFILE_SCHEMA as SCHEMA
|
||||
|
||||
def load(self):
|
||||
if not self.exists():
|
||||
return {}
|
||||
with self.repo.tree.open(self.path) as fd:
|
||||
data = parse_yaml(fd.read(), self.path)
|
||||
try:
|
||||
self.validate(data, fname=self.relpath)
|
||||
except StageFileFormatError:
|
||||
raise LockfileCorruptedError(
|
||||
f"Lockfile '{self.relpath}' is corrupted."
|
||||
)
|
||||
return data
|
||||
|
||||
def dump(self, stage, **kwargs):
|
||||
stage_data = serialize.to_lockfile(stage)
|
||||
if not self.exists():
|
||||
modified = True
|
||||
logger.info("Generating lock file '%s'", self.relpath)
|
||||
data = stage_data
|
||||
open(self.path, "w+").close()
|
||||
else:
|
||||
with self.repo.tree.open(self.path, "r") as fd:
|
||||
data = parse_yaml_for_update(fd.read(), self.path)
|
||||
modified = data.get(stage.name, {}) != stage_data.get(
|
||||
stage.name, {}
|
||||
)
|
||||
if modified:
|
||||
logger.info("Updating lock file '%s'", self.relpath)
|
||||
data.update(stage_data)
|
||||
dump_yaml(self.path, data)
|
||||
if modified:
|
||||
self.repo.scm.track_file(self.relpath)
|
||||
|
||||
def remove_stage(self, stage):
|
||||
if not self.exists():
|
||||
return
|
||||
|
||||
with open(self.path) as f:
|
||||
d = parse_yaml_for_update(f.read(), self.path)
|
||||
self.validate(d, self.path)
|
||||
|
||||
if stage.name not in d:
|
||||
return
|
||||
|
||||
logger.debug("Removing '%s' from '%s'", stage.name, self.path)
|
||||
del d[stage.name]
|
||||
|
||||
if d:
|
||||
dump_yaml(self.path, d)
|
||||
else:
|
||||
self.remove()
|
||||
|
||||
|
||||
class Dvcfile:
|
||||
def __new__(cls, repo, path, **kwargs):
|
||||
assert path
|
||||
assert repo
|
||||
|
||||
_, ext = os.path.splitext(path)
|
||||
if ext in [".yaml", ".yml"]:
|
||||
return PipelineFile(repo, path, **kwargs)
|
||||
# fallback to single stage file for better error messages
|
||||
return SingleStageFile(repo, path, **kwargs)
|
||||
+107
-72
@@ -1,11 +1,20 @@
|
||||
"""Exceptions raised by the dvc."""
|
||||
from funcy import first
|
||||
|
||||
from dvc.utils import relpath
|
||||
from dvc.utils import format_link, relpath
|
||||
|
||||
|
||||
class DvcException(Exception):
|
||||
"""Base class for all dvc exceptions."""
|
||||
|
||||
def __init__(self, msg, *args):
|
||||
assert msg
|
||||
super().__init__(msg, *args)
|
||||
|
||||
|
||||
class InvalidArgumentError(ValueError, DvcException):
|
||||
"""Thrown if arguments are invalid."""
|
||||
|
||||
|
||||
class OutputDuplicationError(DvcException):
|
||||
"""Thrown if a file/directory is specified as an output in more than one
|
||||
@@ -19,12 +28,17 @@ class OutputDuplicationError(DvcException):
|
||||
def __init__(self, output, stages):
|
||||
assert isinstance(output, str)
|
||||
assert all(hasattr(stage, "relpath") for stage in stages)
|
||||
msg = (
|
||||
"file/directory '{}' is specified as an output in more than one "
|
||||
"stage: {}\n"
|
||||
"This is not allowed. Consider using a different output name."
|
||||
).format(output, "\n ".join(s.relpath for s in stages))
|
||||
if len(stages) == 1:
|
||||
msg = "output '{}' is already specified in {}.".format(
|
||||
output, first(stages)
|
||||
)
|
||||
else:
|
||||
msg = "output '{}' is already specified in stages:\n{}".format(
|
||||
output, "\n".join(f"\t- {s.addressing}" for s in stages),
|
||||
)
|
||||
super().__init__(msg)
|
||||
self.stages = stages
|
||||
self.output = output
|
||||
|
||||
|
||||
class OutputNotFoundError(DvcException):
|
||||
@@ -38,7 +52,7 @@ class OutputNotFoundError(DvcException):
|
||||
self.output = output
|
||||
self.repo = repo
|
||||
super().__init__(
|
||||
"unable to find DVC-file with output '{path}'".format(
|
||||
"Unable to find DVC-file with output '{path}'".format(
|
||||
path=relpath(self.output)
|
||||
)
|
||||
)
|
||||
@@ -56,8 +70,8 @@ class StagePathAsOutputError(DvcException):
|
||||
def __init__(self, stage, output):
|
||||
assert isinstance(output, str)
|
||||
super().__init__(
|
||||
"'{stage}' is within an output '{output}' of another stage".format(
|
||||
stage=stage.relpath, output=output
|
||||
"{stage} is within an output '{output}' of another stage".format(
|
||||
stage=stage, output=output
|
||||
)
|
||||
)
|
||||
|
||||
@@ -73,10 +87,7 @@ class CircularDependencyError(DvcException):
|
||||
def __init__(self, dependency):
|
||||
assert isinstance(dependency, str)
|
||||
|
||||
msg = (
|
||||
"file/directory '{}' is specified as an output and as a "
|
||||
"dependency."
|
||||
)
|
||||
msg = "'{}' is specified as an output and as a dependency."
|
||||
super().__init__(msg.format(dependency))
|
||||
|
||||
|
||||
@@ -90,7 +101,7 @@ class ArgumentDuplicationError(DvcException):
|
||||
|
||||
def __init__(self, path):
|
||||
assert isinstance(path, str)
|
||||
super().__init__("file '{}' is specified more than once.".format(path))
|
||||
super().__init__(f"file '{path}' is specified more than once.")
|
||||
|
||||
|
||||
class MoveNotDataSourceError(DvcException):
|
||||
@@ -112,18 +123,7 @@ class MoveNotDataSourceError(DvcException):
|
||||
|
||||
|
||||
class NotDvcRepoError(DvcException):
|
||||
"""Thrown if a directory is not a dvc repo.
|
||||
|
||||
Args:
|
||||
root (str): path to the directory.
|
||||
"""
|
||||
|
||||
def __init__(self, root):
|
||||
msg = (
|
||||
"you are not inside of a dvc repository "
|
||||
"(checked up to mount point '{}')"
|
||||
)
|
||||
super().__init__(msg.format(root))
|
||||
"""Thrown if a directory is not a DVC repo"""
|
||||
|
||||
|
||||
class DvcParserError(DvcException):
|
||||
@@ -136,12 +136,8 @@ class DvcParserError(DvcException):
|
||||
class CyclicGraphError(DvcException):
|
||||
def __init__(self, stages):
|
||||
assert isinstance(stages, list)
|
||||
stages = "\n".join("\t- {}".format(stage.relpath) for stage in stages)
|
||||
msg = (
|
||||
"you've introduced a cycle in your pipeline that involves "
|
||||
"the following stages:"
|
||||
"\n"
|
||||
"{stages}".format(stages=stages)
|
||||
msg = "Pipeline has a cycle involving: {}.".format(
|
||||
", ".join(s.addressing for s in stages)
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
@@ -149,7 +145,7 @@ class CyclicGraphError(DvcException):
|
||||
class ConfirmRemoveError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(
|
||||
"unable to remove '{}' without a confirmation from the user. Use "
|
||||
"unable to remove '{}' without a confirmation. Use "
|
||||
"`-f` to force.".format(path)
|
||||
)
|
||||
|
||||
@@ -161,7 +157,7 @@ class InitError(DvcException):
|
||||
class ReproductionError(DvcException):
|
||||
def __init__(self, dvc_file_name):
|
||||
self.path = dvc_file_name
|
||||
super().__init__("failed to reproduce '{}'".format(dvc_file_name))
|
||||
super().__init__(f"failed to reproduce '{dvc_file_name}'")
|
||||
|
||||
|
||||
class BadMetricError(DvcException):
|
||||
@@ -169,7 +165,7 @@ class BadMetricError(DvcException):
|
||||
super().__init__(
|
||||
"the following metrics do not exist, "
|
||||
"are not metric files or are malformed: {paths}".format(
|
||||
paths=", ".join("'{}'".format(path) for path in paths)
|
||||
paths=", ".join(f"'{path}'" for path in paths)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -177,17 +173,24 @@ class BadMetricError(DvcException):
|
||||
class NoMetricsError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"no metric files in this repository. "
|
||||
"Use `dvc metrics add` to add a metric file to track."
|
||||
"no metric files in this repository. Use `-m/-M` options for "
|
||||
"`dvc run` to mark stage outputs as metrics."
|
||||
)
|
||||
|
||||
|
||||
class StageFileCorruptedError(DvcException):
|
||||
class NoPlotsError(DvcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"no plots in this repository. Use `--plots/--plots-no-cache` "
|
||||
"options for `dvc run` to mark stage outputs as plots."
|
||||
)
|
||||
|
||||
|
||||
class YAMLFileCorruptedError(DvcException):
|
||||
def __init__(self, path):
|
||||
path = relpath(path)
|
||||
super().__init__(
|
||||
"unable to read DVC-file: {} "
|
||||
"YAML file structure is corrupted".format(path)
|
||||
f"unable to read: '{path}', YAML file structure is corrupted"
|
||||
)
|
||||
|
||||
|
||||
@@ -199,22 +202,15 @@ class RecursiveAddingWhileUsingFilename(DvcException):
|
||||
|
||||
|
||||
class OverlappingOutputPathsError(DvcException):
|
||||
def __init__(self, out_1, out_2):
|
||||
super().__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,
|
||||
)
|
||||
)
|
||||
def __init__(self, parent, overlapping_out, message):
|
||||
self.parent = parent
|
||||
self.overlapping_out = overlapping_out
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class CheckoutErrorSuggestGit(DvcException):
|
||||
def __init__(self, target):
|
||||
super().__init__("Did you mean `git checkout {}`?".format(target))
|
||||
super().__init__(f"Did you mean `git checkout {target}`?")
|
||||
|
||||
|
||||
class ETagMismatchError(DvcException):
|
||||
@@ -226,9 +222,11 @@ class ETagMismatchError(DvcException):
|
||||
|
||||
|
||||
class FileMissingError(DvcException):
|
||||
def __init__(self, path):
|
||||
def __init__(self, path, hint=None):
|
||||
self.path = path
|
||||
hint = "" if hint is None else f". {hint}"
|
||||
super().__init__(
|
||||
"Can't find '{}' neither locally nor on remote".format(path)
|
||||
f"Can't find '{path}' neither locally nor on remote{hint}"
|
||||
)
|
||||
|
||||
|
||||
@@ -243,9 +241,10 @@ class DvcIgnoreInCollectedDirError(DvcException):
|
||||
class GitHookAlreadyExistsError(DvcException):
|
||||
def __init__(self, hook_name):
|
||||
super().__init__(
|
||||
"Hook '{}' already exists. Please refer to "
|
||||
"https://man.dvc.org/install "
|
||||
"for more info.".format(hook_name)
|
||||
"Hook '{}' already exists. Please refer to {} for more "
|
||||
"info.".format(
|
||||
hook_name, format_link("https://man.dvc.org/install")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -253,25 +252,23 @@ class DownloadError(DvcException):
|
||||
def __init__(self, amount):
|
||||
self.amount = amount
|
||||
|
||||
super().__init__(
|
||||
"{amount} files failed to download".format(amount=amount)
|
||||
)
|
||||
super().__init__(f"{amount} files failed to download")
|
||||
|
||||
|
||||
class UploadError(DvcException):
|
||||
def __init__(self, amount):
|
||||
self.amount = amount
|
||||
|
||||
super().__init__(
|
||||
"{amount} files failed to upload".format(amount=amount)
|
||||
)
|
||||
super().__init__(f"{amount} files failed to upload")
|
||||
|
||||
|
||||
class CheckoutError(DvcException):
|
||||
def __init__(self, target_infos):
|
||||
def __init__(self, target_infos, stats=None):
|
||||
self.target_infos = target_infos
|
||||
self.stats = stats
|
||||
targets = [str(t) for t in target_infos]
|
||||
m = (
|
||||
"Checkout failed for following targets:\n {}\nDid you "
|
||||
"Checkout failed for following targets:\n{}\nDid you "
|
||||
"forget to fetch?".format("\n".join(targets))
|
||||
)
|
||||
super().__init__(m)
|
||||
@@ -284,7 +281,7 @@ class CollectCacheError(DvcException):
|
||||
class NoRemoteInExternalRepoError(DvcException):
|
||||
def __init__(self, url):
|
||||
super().__init__(
|
||||
"No DVC remote is specified in target repository '{}'.".format(url)
|
||||
f"No DVC remote is specified in target repository '{url}'."
|
||||
)
|
||||
|
||||
|
||||
@@ -299,13 +296,51 @@ class NoOutputInExternalRepoError(DvcException):
|
||||
|
||||
class HTTPError(DvcException):
|
||||
def __init__(self, code, reason):
|
||||
super().__init__("'{} {}'".format(code, reason))
|
||||
super().__init__(f"'{code} {reason}'")
|
||||
|
||||
|
||||
class PathMissingError(DvcException):
|
||||
def __init__(self, path, repo):
|
||||
msg = (
|
||||
"The path '{}' does not exist in the target repository '{}'"
|
||||
" neighther as an output nor a git-handled file."
|
||||
)
|
||||
default_msg = (
|
||||
"The path '{}' does not exist in the target repository '{}'"
|
||||
" neither as a DVC output nor as a Git-tracked file."
|
||||
)
|
||||
default_msg_dvc_only = (
|
||||
"The path '{}' does not exist in the target repository '{}'"
|
||||
" as an DVC output."
|
||||
)
|
||||
|
||||
def __init__(self, path, repo, dvc_only=False):
|
||||
msg = self.default_msg if not dvc_only else self.default_msg_dvc_only
|
||||
super().__init__(msg.format(path, repo))
|
||||
self.dvc_only = dvc_only
|
||||
|
||||
|
||||
class RemoteCacheRequiredError(DvcException):
|
||||
def __init__(self, path_info):
|
||||
super().__init__(
|
||||
(
|
||||
"Current operation was unsuccessful because '{}' requires "
|
||||
"existing cache on '{}' remote. See {} for information on how "
|
||||
"to set up remote cache."
|
||||
).format(
|
||||
path_info,
|
||||
path_info.scheme,
|
||||
format_link("https://man.dvc.org/config#cache"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class IsADirectoryError(DvcException): # noqa,pylint:disable=redefined-builtin
|
||||
"""Raised when a file operation is requested on a directory."""
|
||||
|
||||
|
||||
class NoOutputOrStageError(DvcException):
|
||||
"""
|
||||
Raised when the target is neither an output nor a stage name in dvc.yaml
|
||||
"""
|
||||
|
||||
def __init__(self, target, file):
|
||||
super().__init__(
|
||||
f"'{target}' "
|
||||
f"does not exist as an output or a stage name in '{file}'"
|
||||
)
|
||||
|
||||
+278
-120
@@ -1,27 +1,42 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
from contextlib import contextmanager
|
||||
from distutils.dir_util import copy_tree
|
||||
from typing import Iterable
|
||||
|
||||
from funcy import retry
|
||||
from funcy import cached_property, retry, wrap_with
|
||||
|
||||
from dvc.config import NoRemoteError, ConfigError
|
||||
from dvc.exceptions import NoRemoteInExternalRepoError
|
||||
from dvc.remote import RemoteConfig
|
||||
from dvc.exceptions import NoOutputInExternalRepoError
|
||||
from dvc.exceptions import OutputNotFoundError
|
||||
from dvc.config import NoRemoteError, NotDvcRepoError
|
||||
from dvc.exceptions import (
|
||||
FileMissingError,
|
||||
NoOutputInExternalRepoError,
|
||||
NoRemoteInExternalRepoError,
|
||||
OutputNotFoundError,
|
||||
PathMissingError,
|
||||
)
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.repo import Repo
|
||||
from dvc.repo.tree import RepoTree
|
||||
from dvc.scm.git import Git
|
||||
from dvc.scm.tree import is_working_tree
|
||||
from dvc.utils.fs import remove
|
||||
|
||||
|
||||
REPO_CACHE = {}
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def external_repo(url=None, rev=None, rev_lock=None, cache_dir=None):
|
||||
from dvc.repo import Repo
|
||||
def external_repo(url, rev=None, for_write=False):
|
||||
logger.debug("Creating external repo %s@%s", url, rev)
|
||||
path = _cached_clone(url, rev, for_write=for_write)
|
||||
if not rev:
|
||||
rev = "HEAD"
|
||||
try:
|
||||
repo = ExternalRepo(path, url, rev, for_write=for_write)
|
||||
except NotDvcRepoError:
|
||||
repo = ExternalGitRepo(path, url, rev)
|
||||
|
||||
path = _external_repo(url=url, rev=rev_lock or rev, cache_dir=cache_dir)
|
||||
repo = Repo(path)
|
||||
try:
|
||||
yield repo
|
||||
except NoRemoteError:
|
||||
@@ -30,103 +45,269 @@ def external_repo(url=None, rev=None, rev_lock=None, cache_dir=None):
|
||||
if exc.repo is repo:
|
||||
raise NoOutputInExternalRepoError(exc.output, repo.root_dir, url)
|
||||
raise
|
||||
repo.close()
|
||||
|
||||
|
||||
def cached_clone(url, rev=None, **_ignored_kwargs):
|
||||
"""Clone an external git repo to a temporary directory.
|
||||
|
||||
Returns the path to a local temporary directory with the specified
|
||||
revision checked out.
|
||||
|
||||
Uses the REPO_CACHE to avoid accessing the remote server again if
|
||||
cloning from the same URL twice in the same session.
|
||||
|
||||
"""
|
||||
|
||||
new_path = tempfile.mkdtemp("dvc-erepo")
|
||||
|
||||
# Copy and adjust existing clean clone
|
||||
if (url, None, None) in REPO_CACHE:
|
||||
old_path = REPO_CACHE[url, None, None]
|
||||
|
||||
# This one unlike shutil.copytree() works with an existing dir
|
||||
copy_tree(old_path, new_path)
|
||||
else:
|
||||
# Create a new clone
|
||||
_clone_repo(url, new_path)
|
||||
|
||||
# Save clean clone dir so that we will have access to a default branch
|
||||
clean_clone_path = tempfile.mkdtemp("dvc-erepo")
|
||||
copy_tree(new_path, clean_clone_path)
|
||||
REPO_CACHE[url, None, None] = clean_clone_path
|
||||
|
||||
# Check out the specified revision
|
||||
if rev is not None:
|
||||
_git_checkout(new_path, rev)
|
||||
|
||||
return new_path
|
||||
|
||||
|
||||
def _external_repo(url=None, rev=None, cache_dir=None):
|
||||
from dvc.config import Config
|
||||
from dvc.cache import CacheConfig
|
||||
from dvc.repo import Repo
|
||||
|
||||
key = (url, rev, cache_dir)
|
||||
if key in REPO_CACHE:
|
||||
return REPO_CACHE[key]
|
||||
|
||||
new_path = cached_clone(url, rev=rev)
|
||||
|
||||
repo = Repo(new_path)
|
||||
try:
|
||||
# check if the URL is local and no default remote is present
|
||||
# add default remote pointing to the original repo's cache location
|
||||
if os.path.isdir(url):
|
||||
rconfig = RemoteConfig(repo.config)
|
||||
if not _default_remote_set(rconfig):
|
||||
original_repo = Repo(url)
|
||||
try:
|
||||
rconfig.add(
|
||||
"auto-generated-upstream",
|
||||
original_repo.cache.local.cache_dir,
|
||||
default=True,
|
||||
level=Config.LEVEL_LOCAL,
|
||||
)
|
||||
finally:
|
||||
original_repo.close()
|
||||
|
||||
if cache_dir is not None:
|
||||
cache_config = CacheConfig(repo.config)
|
||||
cache_config.set_dir(cache_dir, level=Config.LEVEL_LOCAL)
|
||||
except FileMissingError as exc:
|
||||
raise PathMissingError(exc.path, url)
|
||||
finally:
|
||||
# Need to close/reopen repo to force config reread
|
||||
repo.close()
|
||||
|
||||
REPO_CACHE[key] = new_path
|
||||
return new_path
|
||||
if for_write:
|
||||
_remove(path)
|
||||
|
||||
|
||||
def _git_checkout(repo_path, revision):
|
||||
from dvc.scm import Git
|
||||
|
||||
git = Git(repo_path)
|
||||
try:
|
||||
git.checkout(revision)
|
||||
finally:
|
||||
git.close()
|
||||
CLONES = {}
|
||||
CACHE_DIRS = {}
|
||||
|
||||
|
||||
def clean_repos():
|
||||
# Outside code should not see cache while we are removing
|
||||
repo_paths = list(REPO_CACHE.values())
|
||||
REPO_CACHE.clear()
|
||||
paths = list(CLONES.values()) + list(CACHE_DIRS.values())
|
||||
CLONES.clear()
|
||||
CACHE_DIRS.clear()
|
||||
|
||||
for path in repo_paths:
|
||||
for path in paths:
|
||||
_remove(path)
|
||||
|
||||
|
||||
class BaseExternalRepo:
|
||||
# pylint: disable=no-member
|
||||
|
||||
_local_cache = None
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
|
||||
@property
|
||||
def local_cache(self):
|
||||
if hasattr(self, "cache"):
|
||||
return self.cache.local
|
||||
return self._local_cache
|
||||
|
||||
@contextmanager
|
||||
def use_cache(self, cache):
|
||||
"""Use the specified cache in place of default tmpdir cache for
|
||||
download operations.
|
||||
"""
|
||||
if hasattr(self, "cache"):
|
||||
save_cache = self.cache.local
|
||||
self.cache.local = cache
|
||||
self._local_cache = cache
|
||||
|
||||
yield
|
||||
|
||||
if hasattr(self, "cache"):
|
||||
self.cache.local = save_cache
|
||||
self._local_cache = None
|
||||
|
||||
@cached_property
|
||||
def repo_tree(self):
|
||||
return RepoTree(self, fetch=True)
|
||||
|
||||
def get_rev(self):
|
||||
if is_working_tree(self.tree):
|
||||
return self.scm.get_rev()
|
||||
if hasattr(self.tree, "tree"):
|
||||
return self.tree.tree.rev
|
||||
return self.tree.rev
|
||||
|
||||
def fetch_external(self, paths: Iterable, **kwargs):
|
||||
"""Fetch specified external repo paths into cache.
|
||||
|
||||
Returns 3-tuple in the form
|
||||
(downloaded, failed, list(cache_infos))
|
||||
where cache_infos can be used as checkout targets for the
|
||||
fetched paths.
|
||||
"""
|
||||
download_results = []
|
||||
failed = 0
|
||||
|
||||
paths = [PathInfo(self.root_dir) / path for path in paths]
|
||||
|
||||
def download_update(result):
|
||||
download_results.append(result)
|
||||
|
||||
save_infos = []
|
||||
for path in paths:
|
||||
if not self.repo_tree.exists(path):
|
||||
raise PathMissingError(path, self.url)
|
||||
save_info = self.local_cache.save(
|
||||
path,
|
||||
self.repo_tree,
|
||||
None,
|
||||
save_link=False,
|
||||
download_callback=download_update,
|
||||
)
|
||||
save_infos.append(save_info)
|
||||
|
||||
return sum(download_results), failed, save_infos
|
||||
|
||||
def get_external(self, path, dest):
|
||||
"""Convenience wrapper for fetch_external and checkout."""
|
||||
if self.local_cache:
|
||||
# fetch DVC and git files to tmpdir cache, then checkout
|
||||
_, _, save_infos = self.fetch_external([path])
|
||||
self.local_cache.checkout(PathInfo(dest), save_infos[0])
|
||||
else:
|
||||
# git-only erepo with no cache, just copy files directly
|
||||
# to dest
|
||||
path = PathInfo(self.root_dir) / path
|
||||
if not self.repo_tree.exists(path):
|
||||
raise PathMissingError(path, self.url)
|
||||
self.repo_tree.copytree(path, dest)
|
||||
|
||||
|
||||
class ExternalRepo(Repo, BaseExternalRepo):
|
||||
def __init__(self, root_dir, url, rev, for_write=False):
|
||||
if for_write:
|
||||
super().__init__(root_dir)
|
||||
else:
|
||||
root_dir = os.path.realpath(root_dir)
|
||||
super().__init__(root_dir, scm=Git(root_dir), rev=rev)
|
||||
self.url = url
|
||||
self._set_cache_dir()
|
||||
self._fix_upstream()
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _set_cache_dir(self):
|
||||
try:
|
||||
cache_dir = CACHE_DIRS[self.url]
|
||||
except KeyError:
|
||||
cache_dir = CACHE_DIRS[self.url] = tempfile.mkdtemp("dvc-cache")
|
||||
|
||||
self.cache.local.cache_dir = cache_dir
|
||||
self._local_cache = self.cache.local
|
||||
|
||||
def _fix_upstream(self):
|
||||
if not os.path.isdir(self.url):
|
||||
return
|
||||
|
||||
try:
|
||||
src_repo = Repo(self.url)
|
||||
except NotDvcRepoError:
|
||||
# If ExternalRepo does not throw NotDvcRepoError and Repo does,
|
||||
# the self.url might be a bare git repo.
|
||||
# NOTE: This will fail to resolve remote with relative path,
|
||||
# same as if it was a remote DVC repo.
|
||||
return
|
||||
|
||||
try:
|
||||
remote_name = self.config["core"].get("remote")
|
||||
if remote_name:
|
||||
self._fix_local_remote(src_repo, remote_name)
|
||||
else:
|
||||
self._add_upstream(src_repo)
|
||||
finally:
|
||||
src_repo.close()
|
||||
|
||||
def _fix_local_remote(self, src_repo, remote_name):
|
||||
# If a remote URL is relative to the source repo,
|
||||
# it will have changed upon config load and made
|
||||
# relative to this new repo. Restore the old one here.
|
||||
new_remote = self.config["remote"][remote_name]
|
||||
old_remote = src_repo.config["remote"][remote_name]
|
||||
if new_remote["url"] != old_remote["url"]:
|
||||
new_remote["url"] = old_remote["url"]
|
||||
|
||||
def _add_upstream(self, src_repo):
|
||||
# Fill the empty upstream entry with a new remote pointing to the
|
||||
# original repo's cache location.
|
||||
cache_dir = src_repo.cache.local.cache_dir
|
||||
self.config["remote"]["auto-generated-upstream"] = {"url": cache_dir}
|
||||
self.config["core"]["remote"] = "auto-generated-upstream"
|
||||
|
||||
|
||||
class ExternalGitRepo(BaseExternalRepo):
|
||||
def __init__(self, root_dir, url, rev):
|
||||
self.root_dir = os.path.realpath(root_dir)
|
||||
self.url = url
|
||||
self.tree = self.scm.get_tree(rev)
|
||||
|
||||
@cached_property
|
||||
def scm(self):
|
||||
return Git(self.root_dir)
|
||||
|
||||
def close(self):
|
||||
if "scm" in self.__dict__:
|
||||
self.scm.close()
|
||||
|
||||
def find_out_by_relpath(self, path):
|
||||
raise OutputNotFoundError(path, self)
|
||||
|
||||
@contextmanager
|
||||
def open_by_relpath(self, path, mode="r", encoding=None, **kwargs):
|
||||
"""Opens a specified resource as a file object."""
|
||||
tree = RepoTree(self)
|
||||
try:
|
||||
with tree.open(
|
||||
path, mode=mode, encoding=encoding, **kwargs
|
||||
) as fobj:
|
||||
yield fobj
|
||||
except FileNotFoundError:
|
||||
raise PathMissingError(path, self.url)
|
||||
|
||||
|
||||
def _cached_clone(url, rev, for_write=False):
|
||||
"""Clone an external git repo to a temporary directory.
|
||||
|
||||
Returns the path to a local temporary directory with the specified
|
||||
revision checked out. If for_write is set prevents reusing this dir via
|
||||
cache.
|
||||
"""
|
||||
# even if we have already cloned this repo, we may need to
|
||||
# fetch/fast-forward to get specified rev
|
||||
clone_path = _clone_default_branch(url, rev)
|
||||
|
||||
if not for_write and (url) in CLONES:
|
||||
return CLONES[url]
|
||||
|
||||
# Copy to a new dir to keep the clone clean
|
||||
repo_path = tempfile.mkdtemp("dvc-erepo")
|
||||
logger.debug("erepo: making a copy of %s clone", url)
|
||||
copy_tree(clone_path, repo_path)
|
||||
|
||||
# Check out the specified revision
|
||||
if for_write:
|
||||
_git_checkout(repo_path, rev)
|
||||
else:
|
||||
CLONES[url] = repo_path
|
||||
return repo_path
|
||||
|
||||
|
||||
@wrap_with(threading.Lock())
|
||||
def _clone_default_branch(url, rev):
|
||||
"""Get or create a clean clone of the url.
|
||||
|
||||
The cloned is reactualized with git pull unless rev is a known sha.
|
||||
"""
|
||||
clone_path = CLONES.get(url)
|
||||
|
||||
git = None
|
||||
try:
|
||||
if clone_path:
|
||||
git = Git(clone_path)
|
||||
# Do not pull for known shas, branches and tags might move
|
||||
if not Git.is_sha(rev) or not git.has_rev(rev):
|
||||
logger.debug("erepo: git pull %s", url)
|
||||
git.pull()
|
||||
else:
|
||||
logger.debug("erepo: git clone %s to a temporary dir", url)
|
||||
clone_path = tempfile.mkdtemp("dvc-clone")
|
||||
git = Git.clone(url, clone_path)
|
||||
CLONES[url] = clone_path
|
||||
finally:
|
||||
if git:
|
||||
git.close()
|
||||
|
||||
return clone_path
|
||||
|
||||
|
||||
def _git_checkout(repo_path, rev):
|
||||
logger.debug("erepo: git checkout %s@%s", repo_path, rev)
|
||||
git = Git(repo_path)
|
||||
try:
|
||||
git.checkout(rev)
|
||||
finally:
|
||||
git.close()
|
||||
|
||||
|
||||
def _remove(path):
|
||||
if os.name == "nt":
|
||||
# git.exe may hang for a while not permitting to remove temp dir
|
||||
@@ -134,26 +315,3 @@ def _remove(path):
|
||||
os_retry(remove)(path)
|
||||
else:
|
||||
remove(path)
|
||||
|
||||
|
||||
def _clone_repo(url, path):
|
||||
from dvc.scm.git import Git
|
||||
|
||||
git = Git.clone(url, path)
|
||||
git.close()
|
||||
|
||||
|
||||
def _default_remote_set(rconfig):
|
||||
"""
|
||||
Checks if default remote config is present.
|
||||
Args:
|
||||
rconfig: a remote config
|
||||
|
||||
Returns:
|
||||
True if the default remote config is set, else False
|
||||
"""
|
||||
try:
|
||||
rconfig.get_default()
|
||||
return True
|
||||
except ConfigError:
|
||||
return False
|
||||
|
||||
+181
-55
@@ -1,17 +1,21 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from itertools import groupby
|
||||
|
||||
from funcy import cached_property
|
||||
from pathspec import PathSpec
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pathspec.util import normalize_file
|
||||
from pygtrie import StringTrie
|
||||
|
||||
from dvc.scm.tree import BaseTree
|
||||
from dvc.path_info import PathInfo
|
||||
from dvc.pathspec_math import merge_patterns
|
||||
from dvc.system import System
|
||||
from dvc.utils import relpath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DvcIgnore(object):
|
||||
class DvcIgnore:
|
||||
DVCIGNORE_FILE = ".dvcignore"
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
@@ -19,37 +23,110 @@ class DvcIgnore(object):
|
||||
|
||||
|
||||
class DvcIgnorePatterns(DvcIgnore):
|
||||
def __init__(self, ignore_file_path, tree):
|
||||
def __init__(self, pattern_list, dirname):
|
||||
|
||||
self.pattern_list = pattern_list
|
||||
self.dirname = dirname
|
||||
self.prefix = self.dirname + os.sep
|
||||
|
||||
regex_pattern_list = map(
|
||||
GitWildMatchPattern.pattern_to_regex, pattern_list
|
||||
)
|
||||
|
||||
self.ignore_spec = [
|
||||
(ignore, re.compile("|".join(item[0] for item in group)))
|
||||
for ignore, group in groupby(regex_pattern_list, lambda x: x[1])
|
||||
if ignore is not None
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def from_files(cls, ignore_file_path, tree):
|
||||
assert os.path.isabs(ignore_file_path)
|
||||
|
||||
self.ignore_file_path = ignore_file_path
|
||||
self.dirname = os.path.normpath(os.path.dirname(ignore_file_path))
|
||||
|
||||
dirname = os.path.normpath(os.path.dirname(ignore_file_path))
|
||||
with tree.open(ignore_file_path, encoding="utf-8") as fobj:
|
||||
self.ignore_spec = PathSpec.from_lines(GitWildMatchPattern, fobj)
|
||||
path_spec_lines = [
|
||||
line for line in map(str.strip, fobj.readlines()) if line
|
||||
]
|
||||
|
||||
return cls(path_spec_lines, dirname)
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
files = [f for f in files if not self.matches(root, f)]
|
||||
dirs = [d for d in dirs if not self.matches(root, d)]
|
||||
dirs = [d for d in dirs if not self.matches(root, d, True)]
|
||||
|
||||
return dirs, files
|
||||
|
||||
def matches(self, dirname, basename):
|
||||
abs_path = os.path.join(dirname, basename)
|
||||
rel_path = relpath(abs_path, self.dirname)
|
||||
|
||||
if os.pardir + os.sep in rel_path:
|
||||
def matches(self, dirname, basename, is_dir=False):
|
||||
# NOTE: `relpath` is too slow, so we have to assume that both
|
||||
# `dirname` and `self.dirname` are relative or absolute together.
|
||||
if dirname == self.dirname:
|
||||
path = basename
|
||||
elif dirname.startswith(self.prefix):
|
||||
rel = dirname[len(self.prefix) :]
|
||||
# NOTE: `os.path.join` is ~x5.5 slower
|
||||
path = f"{rel}{os.sep}{basename}"
|
||||
else:
|
||||
return False
|
||||
return self.ignore_spec.match_file(rel_path)
|
||||
|
||||
if not System.is_unix():
|
||||
path = normalize_file(path)
|
||||
return self.ignore(path, is_dir)
|
||||
|
||||
def ignore(self, path, is_dir):
|
||||
result = False
|
||||
if is_dir:
|
||||
path_dir = f"{path}/"
|
||||
for ignore, pattern in self.ignore_spec:
|
||||
if pattern.match(path) or pattern.match(path_dir):
|
||||
result = ignore
|
||||
else:
|
||||
for ignore, pattern in self.ignore_spec:
|
||||
if pattern.match(path):
|
||||
result = ignore
|
||||
return result
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.ignore_file_path)
|
||||
return hash(self.dirname + ":" + "\n".join(self.pattern_list))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, DvcIgnorePatterns):
|
||||
return NotImplemented
|
||||
return (self.dirname == other.dirname) & (
|
||||
self.pattern_list == other.pattern_list
|
||||
)
|
||||
|
||||
return self.ignore_file_path == other.ignore_file_path
|
||||
def __bool__(self):
|
||||
return bool(self.pattern_list)
|
||||
|
||||
|
||||
class DvcIgnorePatternsTrie(DvcIgnore):
|
||||
trie = None
|
||||
|
||||
def __init__(self):
|
||||
if self.trie is None:
|
||||
self.trie = StringTrie(separator=os.sep)
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
ignore_pattern = self[root]
|
||||
if ignore_pattern:
|
||||
return ignore_pattern(root, dirs, files)
|
||||
return dirs, files
|
||||
|
||||
def __setitem__(self, root, ignore_pattern):
|
||||
base_pattern = self[root]
|
||||
common_dirname, merged_pattern = merge_patterns(
|
||||
base_pattern.dirname,
|
||||
base_pattern.pattern_list,
|
||||
ignore_pattern.dirname,
|
||||
ignore_pattern.pattern_list,
|
||||
)
|
||||
self.trie[root] = DvcIgnorePatterns(merged_pattern, common_dirname)
|
||||
|
||||
def __getitem__(self, root):
|
||||
ignore_pattern = self.trie.longest_prefix(root)
|
||||
if ignore_pattern:
|
||||
return ignore_pattern.value
|
||||
return DvcIgnorePatterns([], root)
|
||||
|
||||
|
||||
class DvcIgnoreDirs(DvcIgnore):
|
||||
@@ -71,18 +148,53 @@ class DvcIgnoreDirs(DvcIgnore):
|
||||
return self.basenames == other.basenames
|
||||
|
||||
|
||||
class DvcIgnoreFilter(object):
|
||||
def __init__(self, tree):
|
||||
self.tree = tree
|
||||
self.ignores = {DvcIgnoreDirs([".git", ".hg", ".dvc"])}
|
||||
for root, dirs, files in self.tree.walk(self.tree.tree_root):
|
||||
self._update(root)
|
||||
dirs[:], files[:] = self(root, dirs, files)
|
||||
class DvcIgnoreRepo(DvcIgnore):
|
||||
def __call__(self, root, dirs, files):
|
||||
def is_dvc_repo(directory):
|
||||
from dvc.repo import Repo
|
||||
|
||||
def _update(self, dirname):
|
||||
return os.path.isdir(os.path.join(root, directory, Repo.DVC_DIR))
|
||||
|
||||
dirs = [d for d in dirs if not is_dvc_repo(d)]
|
||||
|
||||
return dirs, files
|
||||
|
||||
|
||||
class DvcIgnoreFilterNoop:
|
||||
def __init__(self, tree, root_dir):
|
||||
pass
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
return dirs, files
|
||||
|
||||
def is_ignored_dir(self, _):
|
||||
return False
|
||||
|
||||
def is_ignored_file(self, _):
|
||||
return False
|
||||
|
||||
|
||||
class DvcIgnoreFilter:
|
||||
def __init__(self, tree, root_dir):
|
||||
self.tree = tree
|
||||
self.root_dir = root_dir
|
||||
self.ignores = {
|
||||
DvcIgnoreDirs([".git", ".hg", ".dvc"]),
|
||||
DvcIgnoreRepo(),
|
||||
}
|
||||
ignore_pattern_trie = DvcIgnorePatternsTrie()
|
||||
for root, dirs, _ in self.tree.walk(self.root_dir):
|
||||
ignore_pattern = self._get_ignore_pattern(root)
|
||||
if ignore_pattern:
|
||||
ignore_pattern_trie[root] = ignore_pattern
|
||||
self.ignores.add(ignore_pattern_trie)
|
||||
dirs[:], _ = self(root, dirs, [])
|
||||
|
||||
def _get_ignore_pattern(self, dirname):
|
||||
ignore_file_path = os.path.join(dirname, DvcIgnore.DVCIGNORE_FILE)
|
||||
if self.tree.exists(ignore_file_path):
|
||||
self.ignores.add(DvcIgnorePatterns(ignore_file_path, self.tree))
|
||||
return DvcIgnorePatterns.from_files(ignore_file_path, self.tree)
|
||||
return None
|
||||
|
||||
def __call__(self, root, dirs, files):
|
||||
for ignore in self.ignores:
|
||||
@@ -90,38 +202,52 @@ class DvcIgnoreFilter(object):
|
||||
|
||||
return dirs, files
|
||||
|
||||
def is_ignored_dir(self, path):
|
||||
if not self._parents_exist(path):
|
||||
return True
|
||||
|
||||
class CleanTree(BaseTree):
|
||||
def __init__(self, tree):
|
||||
self.tree = tree
|
||||
path = os.path.abspath(path)
|
||||
if path == self.root_dir:
|
||||
return False
|
||||
dirname, basename = os.path.split(path)
|
||||
dirs, _ = self(dirname, [basename], [])
|
||||
return not dirs
|
||||
|
||||
@cached_property
|
||||
def dvcignore(self):
|
||||
return DvcIgnoreFilter(self.tree)
|
||||
def is_ignored_file(self, path):
|
||||
if not self._parents_exist(path):
|
||||
return True
|
||||
|
||||
@property
|
||||
def tree_root(self):
|
||||
return self.tree.tree_root
|
||||
dirname, basename = os.path.split(os.path.normpath(path))
|
||||
_, files = self(os.path.abspath(dirname), [], [basename])
|
||||
return not files
|
||||
|
||||
def open(self, path, mode="r", encoding="utf-8"):
|
||||
return self.tree.open(path, mode, encoding)
|
||||
def _parents_exist(self, path):
|
||||
from dvc.repo import Repo
|
||||
|
||||
def exists(self, path):
|
||||
return self.tree.exists(path)
|
||||
path = PathInfo(path)
|
||||
|
||||
def isdir(self, path):
|
||||
return self.tree.isdir(path)
|
||||
# if parent is root_dir or inside a .dvc dir we can skip this check
|
||||
if path.parent == self.root_dir or Repo.DVC_DIR in path.parts:
|
||||
return True
|
||||
|
||||
def isfile(self, path):
|
||||
return self.tree.isfile(path)
|
||||
# paths outside of the repo should be ignored
|
||||
path = relpath(path, self.root_dir)
|
||||
if path.startswith("..") or (
|
||||
os.name == "nt"
|
||||
and not os.path.commonprefix(
|
||||
[os.path.abspath(path), self.root_dir]
|
||||
)
|
||||
):
|
||||
return False
|
||||
|
||||
def walk(self, top, topdown=True):
|
||||
for root, dirs, files in self.tree.walk(top, topdown):
|
||||
dirs[:], files[:] = self.dvcignore(root, dirs, files)
|
||||
|
||||
yield root, dirs, files
|
||||
|
||||
def walk_files(self, top):
|
||||
for root, _, files in self.walk(top):
|
||||
for file in files:
|
||||
yield os.path.join(root, file)
|
||||
# check if parent directories are in our ignores, starting from
|
||||
# root_dir
|
||||
for parent_dir in reversed(PathInfo(path).parents):
|
||||
dirname, basename = os.path.split(parent_dir)
|
||||
if basename == ".":
|
||||
# parent_dir == root_dir
|
||||
continue
|
||||
dirs, _ = self(os.path.abspath(dirname), [basename], [])
|
||||
if not dirs:
|
||||
return False
|
||||
return True
|
||||
|
||||
+6
-2
@@ -7,13 +7,17 @@
|
||||
TEXT_CHARS = bytes(range(32, 127)) + b"\n\r\t\f\b"
|
||||
|
||||
|
||||
def istextfile(fname, blocksize=512):
|
||||
def istextfile(fname, blocksize=512, tree=None):
|
||||
""" Uses heuristics to guess whether the given file is text or binary,
|
||||
by reading a single block of bytes from the file.
|
||||
If more than 30% of the chars in the block are non-text, or there
|
||||
are NUL ('\x00') bytes in the block, assume this is a binary file.
|
||||
"""
|
||||
with open(fname, "rb") as fobj:
|
||||
if tree:
|
||||
open_func = tree.open
|
||||
else:
|
||||
open_func = open
|
||||
with open_func(fname, "rb") as fobj:
|
||||
block = fobj.read(blocksize)
|
||||
|
||||
if not block:
|
||||
|
||||
+12
-10
@@ -5,28 +5,28 @@ import os
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
import zc.lockfile
|
||||
import flufl.lock
|
||||
import zc.lockfile
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import format_link
|
||||
from dvc.progress import Tqdm
|
||||
from dvc.utils import format_link
|
||||
|
||||
DEFAULT_TIMEOUT = 5
|
||||
|
||||
FAILED_TO_LOCK_MESSAGE = (
|
||||
"cannot perform the command because another DVC process seems to be "
|
||||
"running on this project. If that is not the case, manually remove "
|
||||
"`.dvc/lock` and try again."
|
||||
"`.dvc/tmp/lock` and try again."
|
||||
)
|
||||
|
||||
|
||||
class LockError(DvcException):
|
||||
"""Thrown when unable to acquire the lock for dvc repo."""
|
||||
"""Thrown when unable to acquire the lock for DVC repo."""
|
||||
|
||||
|
||||
class Lock(object):
|
||||
"""Class for dvc repo lock.
|
||||
class Lock:
|
||||
"""Class for DVC repo lock.
|
||||
|
||||
Uses zc.lockfile as backend.
|
||||
"""
|
||||
@@ -47,7 +47,7 @@ class Lock(object):
|
||||
disable=not self._friendly,
|
||||
desc=(
|
||||
"If DVC froze, see `hardlink_lock` in {}".format(
|
||||
format_link("man.dvc.org/config#core")
|
||||
format_link("https://man.dvc.org/config#core")
|
||||
)
|
||||
),
|
||||
):
|
||||
@@ -80,7 +80,7 @@ class Lock(object):
|
||||
|
||||
|
||||
class HardlinkLock(flufl.lock.Lock):
|
||||
"""Class for dvc repo lock.
|
||||
"""Class for DVC repo lock.
|
||||
|
||||
Args:
|
||||
lockfile (str): the lock filename
|
||||
@@ -88,7 +88,9 @@ class HardlinkLock(flufl.lock.Lock):
|
||||
tmp_dir (str): a directory to store claim files.
|
||||
"""
|
||||
|
||||
def __init__(self, lockfile, tmp_dir=None, **kwargs):
|
||||
def __init__(
|
||||
self, lockfile, tmp_dir=None, **kwargs
|
||||
): # pylint: disable=super-init-not-called
|
||||
import socket
|
||||
|
||||
self._tmp_dir = tmp_dir
|
||||
@@ -117,7 +119,7 @@ class HardlinkLock(flufl.lock.Lock):
|
||||
def lockfile(self):
|
||||
return self._lockfile
|
||||
|
||||
def lock(self):
|
||||
def lock(self): # pylint: disable=arguments-differ
|
||||
try:
|
||||
super().lock(timedelta(seconds=DEFAULT_TIMEOUT))
|
||||
except flufl.lock.TimeOutError:
|
||||
|
||||
+121
-102
@@ -1,6 +1,5 @@
|
||||
"""Manages logging configuration for dvc repo."""
|
||||
"""Manages logging configuration for DVC repo."""
|
||||
|
||||
import traceback
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
|
||||
@@ -8,7 +7,6 @@ import colorama
|
||||
|
||||
from dvc.progress import Tqdm
|
||||
|
||||
|
||||
FOOTER = (
|
||||
"\n{yellow}Having any troubles?{nc}"
|
||||
" Hit us up at {blue}https://dvc.org/support{nc},"
|
||||
@@ -20,121 +18,109 @@ FOOTER = (
|
||||
)
|
||||
|
||||
|
||||
def addLoggingLevel(levelName, levelNum, methodName=None):
|
||||
"""
|
||||
Adds a new logging level to the `logging` module and the
|
||||
currently configured logging class.
|
||||
|
||||
Based on https://stackoverflow.com/questions/2183233
|
||||
"""
|
||||
if methodName is None:
|
||||
methodName = levelName.lower()
|
||||
|
||||
assert not hasattr(logging, levelName)
|
||||
assert not hasattr(logging, methodName)
|
||||
assert not hasattr(logging.getLoggerClass(), methodName)
|
||||
|
||||
def logForLevel(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(levelNum):
|
||||
# pylint: disable=protected-access
|
||||
self._log(levelNum, message, args, **kwargs)
|
||||
|
||||
def logToRoot(message, *args, **kwargs):
|
||||
logging.log(levelNum, message, *args, **kwargs)
|
||||
|
||||
logging.addLevelName(levelNum, levelName)
|
||||
setattr(logging, levelName, levelNum)
|
||||
setattr(logging.getLoggerClass(), methodName, logForLevel)
|
||||
setattr(logging, methodName, logToRoot)
|
||||
|
||||
|
||||
class LoggingException(Exception):
|
||||
def __init__(self, record):
|
||||
msg = "failed to log {}".format(str(record))
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ExcludeErrorsFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
return record.levelno < logging.WARNING
|
||||
def excludeFilter(level):
|
||||
class ExcludeLevelFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
return record.levelno < level
|
||||
|
||||
|
||||
class ExcludeInfoFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
return record.levelno < logging.INFO
|
||||
return ExcludeLevelFilter
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
"""Enable color support when logging to a terminal that supports it.
|
||||
"""Spit out colored text in supported terminals.
|
||||
|
||||
Color support on Windows versions that do not support ANSI color codes is
|
||||
enabled by use of the colorama__ library.
|
||||
colorama__ makes ANSI escape character sequences work under Windows.
|
||||
See the colorama documentation for details.
|
||||
|
||||
__ https://pypi.python.org/pypi/colorama
|
||||
|
||||
For records containing `exc_info`, it will use a custom `_walk_exc` to
|
||||
retrieve the whole traceback.
|
||||
If record has an extra `tb_only` attribute, it will not show the
|
||||
exception cause, just the message and the traceback.
|
||||
"""
|
||||
|
||||
color_code = {
|
||||
"TRACE": colorama.Fore.GREEN,
|
||||
"DEBUG": colorama.Fore.BLUE,
|
||||
"INFO": "",
|
||||
"WARNING": colorama.Fore.YELLOW,
|
||||
"ERROR": colorama.Fore.RED,
|
||||
"CRITICAL": colorama.Fore.RED,
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
record.message = record.getMessage()
|
||||
msg = self.formatMessage(record)
|
||||
|
||||
if record.levelname == "INFO":
|
||||
return record.msg
|
||||
return msg
|
||||
|
||||
if record.levelname == "ERROR" or record.levelname == "CRITICAL":
|
||||
exception, stack_trace = self._parse_exc(record)
|
||||
if record.exc_info:
|
||||
if getattr(record, "tb_only", False):
|
||||
cause = ""
|
||||
else:
|
||||
cause = ": ".join(_iter_causes(record.exc_info[1]))
|
||||
|
||||
return (
|
||||
"{color}{levelname}{nc}: {description}" "{stack_trace}\n"
|
||||
).format(
|
||||
color=self.color_code.get(record.levelname, ""),
|
||||
nc=colorama.Fore.RESET,
|
||||
levelname=record.levelname,
|
||||
description=self._description(record.msg, exception),
|
||||
msg=record.msg,
|
||||
stack_trace=stack_trace,
|
||||
msg = "{message}{separator}{cause}".format(
|
||||
message=msg or "",
|
||||
separator=" - " if msg and cause else "",
|
||||
cause=cause,
|
||||
)
|
||||
|
||||
return "{color}{levelname}{nc}: {msg}".format(
|
||||
color=self.color_code.get(record.levelname, ""),
|
||||
if _is_verbose():
|
||||
msg += _stack_trace(record.exc_info)
|
||||
|
||||
return "{asctime}{color}{levelname}{nc}: {msg}".format(
|
||||
asctime=self.formatTime(record, self.datefmt),
|
||||
color=self.color_code[record.levelname],
|
||||
nc=colorama.Fore.RESET,
|
||||
levelname=record.levelname,
|
||||
msg=record.msg,
|
||||
msg=msg,
|
||||
)
|
||||
|
||||
def _current_level(self):
|
||||
return logging.getLogger("dvc").getEffectiveLevel()
|
||||
def formatTime(self, record, datefmt=None):
|
||||
# only show if current level is set to DEBUG
|
||||
# also, skip INFO as it is used for UI
|
||||
if not _is_verbose() or record.levelno == logging.INFO:
|
||||
return ""
|
||||
|
||||
def _is_visible(self, record):
|
||||
return record.levelno >= self._current_level()
|
||||
|
||||
def _description(self, message, exception):
|
||||
description = ""
|
||||
|
||||
if exception and message:
|
||||
description = "{message} - {exception}"
|
||||
elif exception:
|
||||
description = "{exception}"
|
||||
elif message:
|
||||
description = "{message}"
|
||||
|
||||
return description.format(message=message, exception=exception)
|
||||
|
||||
def _walk_exc(self, exc_info):
|
||||
exc = exc_info[1]
|
||||
|
||||
exc_list = [str(exc)]
|
||||
|
||||
while hasattr(exc, "__cause__") and exc.__cause__:
|
||||
exc_list.append(str(exc.__cause__))
|
||||
exc = exc.__cause__
|
||||
|
||||
return exc_list
|
||||
|
||||
def _parse_exc(self, record):
|
||||
tb_only = getattr(record, "tb_only", False)
|
||||
|
||||
if not record.exc_info:
|
||||
return (None, "")
|
||||
|
||||
exc_list = self._walk_exc(record.exc_info)
|
||||
tb = traceback.format_exception(*record.exc_info)
|
||||
|
||||
exception = None if tb_only else ": ".join(exc_list)
|
||||
|
||||
if self._current_level() == logging.DEBUG:
|
||||
stack_trace = (
|
||||
"\n" "{red}{line}{nc}\n" "{stack_trace}" "{red}{line}{nc}"
|
||||
).format(
|
||||
red=colorama.Fore.RED,
|
||||
nc=colorama.Fore.RESET,
|
||||
line="-" * 60,
|
||||
stack_trace="".join(tb),
|
||||
)
|
||||
else:
|
||||
stack_trace = ""
|
||||
|
||||
return (exception, stack_trace)
|
||||
return "{green}{date}{nc} ".format(
|
||||
green=colorama.Fore.GREEN,
|
||||
date=super().formatTime(record, datefmt),
|
||||
nc=colorama.Fore.RESET,
|
||||
)
|
||||
|
||||
|
||||
class LoggerHandler(logging.StreamHandler):
|
||||
@@ -152,19 +138,59 @@ class LoggerHandler(logging.StreamHandler):
|
||||
self.flush()
|
||||
except RecursionError:
|
||||
raise
|
||||
except Exception:
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
self.handleError(record)
|
||||
|
||||
|
||||
def _is_verbose():
|
||||
return (
|
||||
logging.NOTSET
|
||||
< logging.getLogger("dvc").getEffectiveLevel()
|
||||
<= logging.DEBUG
|
||||
)
|
||||
|
||||
|
||||
def _iter_causes(exc):
|
||||
while exc:
|
||||
yield str(exc)
|
||||
exc = exc.__cause__
|
||||
|
||||
|
||||
def _stack_trace(exc_info):
|
||||
import traceback
|
||||
|
||||
return (
|
||||
"\n"
|
||||
"{red}{line}{nc}\n"
|
||||
"{trace}"
|
||||
"{red}{line}{nc}".format(
|
||||
red=colorama.Fore.RED,
|
||||
line="-" * 60,
|
||||
trace="".join(traceback.format_exception(*exc_info)),
|
||||
nc=colorama.Fore.RESET,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def disable_other_loggers():
|
||||
logging.captureWarnings(True)
|
||||
root = logging.root
|
||||
for (logger_name, logger) in root.manager.loggerDict.items():
|
||||
if logger_name != "dvc" and not logger_name.startswith("dvc."):
|
||||
logger.disabled = True
|
||||
|
||||
|
||||
def setup(level=logging.INFO):
|
||||
colorama.init()
|
||||
|
||||
addLoggingLevel("TRACE", logging.DEBUG - 5)
|
||||
logging.config.dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"filters": {
|
||||
"exclude_errors": {"()": ExcludeErrorsFilter},
|
||||
"exclude_info": {"()": ExcludeInfoFilter},
|
||||
"exclude_errors": {"()": excludeFilter(logging.WARNING)},
|
||||
"exclude_info": {"()": excludeFilter(logging.INFO)},
|
||||
"exclude_debug": {"()": excludeFilter(logging.DEBUG)},
|
||||
},
|
||||
"formatters": {"color": {"()": ColorFormatter}},
|
||||
"handlers": {
|
||||
@@ -182,6 +208,13 @@ def setup(level=logging.INFO):
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_info"],
|
||||
},
|
||||
"console_trace": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "TRACE",
|
||||
"formatter": "color",
|
||||
"stream": "ext://sys.stdout",
|
||||
"filters": ["exclude_debug"],
|
||||
},
|
||||
"console_errors": {
|
||||
"class": "dvc.logger.LoggerHandler",
|
||||
"level": "WARNING",
|
||||
@@ -195,25 +228,11 @@ def setup(level=logging.INFO):
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
"paramiko": {
|
||||
"level": logging.CRITICAL,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
"flufl.lock": {
|
||||
"level": logging.CRITICAL,
|
||||
"handlers": [
|
||||
"console_info",
|
||||
"console_debug",
|
||||
"console_trace",
|
||||
"console_errors",
|
||||
],
|
||||
},
|
||||
},
|
||||
"disable_existing_loggers": False,
|
||||
}
|
||||
)
|
||||
|
||||
+35
-14
@@ -6,17 +6,17 @@ import logging
|
||||
from dvc import analytics
|
||||
from dvc.cli import parse_args
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcParserError
|
||||
from dvc.exceptions import NotDvcRepoError
|
||||
from dvc.exceptions import DvcException, DvcParserError, NotDvcRepoError
|
||||
from dvc.external_repo import clean_repos
|
||||
from dvc.logger import FOOTER
|
||||
|
||||
from dvc.logger import FOOTER, disable_other_loggers
|
||||
from dvc.tree.pool import close_pools
|
||||
from dvc.utils import format_link
|
||||
|
||||
# Workaround for CPython bug. See [1] and [2] for more info.
|
||||
# [1] https://github.com/aws/aws-cli/blob/1.16.277/awscli/clidriver.py#L55
|
||||
# [2] https://bugs.python.org/issue29288
|
||||
"".encode("idna")
|
||||
|
||||
"".encode("idna")
|
||||
|
||||
logger = logging.getLogger("dvc")
|
||||
|
||||
@@ -31,17 +31,23 @@ def main(argv=None):
|
||||
int: command's return code.
|
||||
"""
|
||||
args = None
|
||||
cmd = None
|
||||
disable_other_loggers()
|
||||
|
||||
outerLogLevel = logger.level
|
||||
try:
|
||||
args = parse_args(argv)
|
||||
|
||||
if args.quiet:
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
|
||||
elif args.verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
verbosity = args.verbose - args.quiet
|
||||
if verbosity:
|
||||
logger.setLevel(
|
||||
{
|
||||
-2: logging.CRITICAL,
|
||||
-1: logging.ERROR,
|
||||
1: logging.DEBUG,
|
||||
2: logging.TRACE,
|
||||
}[max(-2, min(verbosity, 2))]
|
||||
)
|
||||
logger.trace(args)
|
||||
|
||||
cmd = args.func(args)
|
||||
ret = cmd.run()
|
||||
@@ -56,18 +62,33 @@ def main(argv=None):
|
||||
ret = 253
|
||||
except DvcParserError:
|
||||
ret = 254
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
if isinstance(exc, OSError) and exc.errno == errno.EMFILE:
|
||||
except DvcException:
|
||||
ret = 255
|
||||
logger.exception("")
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EMFILE:
|
||||
logger.exception(
|
||||
"too many open files, please increase your `ulimit`",
|
||||
"too many open files, please visit "
|
||||
"{} to see how to handle this "
|
||||
"problem".format(
|
||||
format_link("https://error.dvc.org/many-files")
|
||||
),
|
||||
extra={"tb_only": True},
|
||||
)
|
||||
else:
|
||||
logger.exception("unexpected error")
|
||||
ret = 255
|
||||
except Exception: # noqa, pylint: disable=broad-except
|
||||
logger.exception("unexpected error")
|
||||
ret = 255
|
||||
finally:
|
||||
logger.setLevel(outerLogLevel)
|
||||
|
||||
# Closing pools by-hand to prevent weird messages when closing SSH
|
||||
# connections. See https://github.com/iterative/dvc/issues/3248 for
|
||||
# more info.
|
||||
close_pools()
|
||||
|
||||
# Remove cached repos in the end of the call, these are anonymous
|
||||
# so won't be reused by any other subsequent run anyway.
|
||||
clean_repos()
|
||||
|
||||
+115
-62
@@ -1,34 +1,44 @@
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urlparse
|
||||
from voluptuous import Any, Required
|
||||
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.output.gs import OutputGS
|
||||
from dvc.output.hdfs import OutputHDFS
|
||||
from dvc.output.local import OutputLOCAL
|
||||
from dvc.output.s3 import OutputS3
|
||||
from dvc.output.ssh import OutputSSH
|
||||
from dvc.remote import Remote
|
||||
from dvc.remote.hdfs import RemoteHDFS
|
||||
from dvc.remote.local import RemoteLOCAL
|
||||
from dvc.remote.s3 import RemoteS3
|
||||
from funcy import collecting, project
|
||||
from voluptuous import And, Any, Coerce, Length, Lower, Required, SetTo
|
||||
|
||||
from dvc.output.base import BaseOutput
|
||||
from dvc.output.gs import GSOutput
|
||||
from dvc.output.hdfs import HDFSOutput
|
||||
from dvc.output.local import LocalOutput
|
||||
from dvc.output.s3 import S3Output
|
||||
from dvc.output.ssh import SSHOutput
|
||||
from dvc.scheme import Schemes
|
||||
|
||||
from ..tree import get_cloud_tree
|
||||
from ..tree.hdfs import HDFSRemoteTree
|
||||
from ..tree.local import LocalRemoteTree
|
||||
from ..tree.s3 import S3RemoteTree
|
||||
|
||||
OUTS = [
|
||||
OutputHDFS,
|
||||
OutputS3,
|
||||
OutputGS,
|
||||
OutputSSH,
|
||||
# NOTE: OutputLOCAL is the default choice
|
||||
HDFSOutput,
|
||||
S3Output,
|
||||
GSOutput,
|
||||
SSHOutput,
|
||||
# NOTE: LocalOutput is the default choice
|
||||
]
|
||||
|
||||
OUTS_MAP = {
|
||||
Schemes.HDFS: OutputHDFS,
|
||||
Schemes.S3: OutputS3,
|
||||
Schemes.GS: OutputGS,
|
||||
Schemes.SSH: OutputSSH,
|
||||
Schemes.LOCAL: OutputLOCAL,
|
||||
Schemes.HDFS: HDFSOutput,
|
||||
Schemes.S3: S3Output,
|
||||
Schemes.GS: GSOutput,
|
||||
Schemes.SSH: SSHOutput,
|
||||
Schemes.LOCAL: LocalOutput,
|
||||
}
|
||||
|
||||
CHECKSUM_SCHEMA = Any(
|
||||
None,
|
||||
And(str, Length(max=0), SetTo(None)),
|
||||
And(Any(str, And(int, Coerce(str))), Length(min=3), Lower),
|
||||
)
|
||||
|
||||
# NOTE: currently there are only 3 possible checksum names:
|
||||
#
|
||||
# 1) md5 (LOCAL, SSH, GS);
|
||||
@@ -37,36 +47,36 @@ OUTS_MAP = {
|
||||
#
|
||||
# so when a few types of outputs share the same name, we only need
|
||||
# specify it once.
|
||||
CHECKSUM_SCHEMA = {
|
||||
RemoteLOCAL.PARAM_CHECKSUM: Any(str, None),
|
||||
RemoteS3.PARAM_CHECKSUM: Any(str, None),
|
||||
RemoteHDFS.PARAM_CHECKSUM: Any(str, None),
|
||||
CHECKSUMS_SCHEMA = {
|
||||
LocalRemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
S3RemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
HDFSRemoteTree.PARAM_CHECKSUM: CHECKSUM_SCHEMA,
|
||||
}
|
||||
|
||||
TAGS_SCHEMA = {str: CHECKSUM_SCHEMA}
|
||||
|
||||
SCHEMA = CHECKSUM_SCHEMA.copy()
|
||||
SCHEMA[Required(OutputBase.PARAM_PATH)] = str
|
||||
SCHEMA[OutputBase.PARAM_CACHE] = bool
|
||||
SCHEMA[OutputBase.PARAM_METRIC] = OutputBase.METRIC_SCHEMA
|
||||
SCHEMA[OutputBase.PARAM_TAGS] = TAGS_SCHEMA
|
||||
SCHEMA[OutputBase.PARAM_PERSIST] = bool
|
||||
SCHEMA = CHECKSUMS_SCHEMA.copy()
|
||||
SCHEMA[Required(BaseOutput.PARAM_PATH)] = str
|
||||
SCHEMA[BaseOutput.PARAM_CACHE] = bool
|
||||
SCHEMA[BaseOutput.PARAM_METRIC] = BaseOutput.METRIC_SCHEMA
|
||||
SCHEMA[BaseOutput.PARAM_PLOT] = bool
|
||||
SCHEMA[BaseOutput.PARAM_PERSIST] = bool
|
||||
|
||||
|
||||
def _get(stage, p, info, cache, metric, persist=False, tags=None):
|
||||
def _get(
|
||||
stage, p, info=None, cache=True, metric=False, plot=False, persist=False
|
||||
):
|
||||
parsed = urlparse(p)
|
||||
|
||||
if parsed.scheme == "remote":
|
||||
remote = Remote(stage.repo, name=parsed.netloc)
|
||||
return OUTS_MAP[remote.scheme](
|
||||
tree = get_cloud_tree(stage.repo, name=parsed.netloc)
|
||||
return OUTS_MAP[tree.scheme](
|
||||
stage,
|
||||
p,
|
||||
info,
|
||||
cache=cache,
|
||||
remote=remote,
|
||||
tree=tree,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
for o in OUTS:
|
||||
@@ -76,31 +86,31 @@ def _get(stage, p, info, cache, metric, persist=False, tags=None):
|
||||
p,
|
||||
info,
|
||||
cache=cache,
|
||||
remote=None,
|
||||
tree=None,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
tags=tags,
|
||||
)
|
||||
return OutputLOCAL(
|
||||
return LocalOutput(
|
||||
stage,
|
||||
p,
|
||||
info,
|
||||
cache=cache,
|
||||
remote=None,
|
||||
tree=None,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
def loadd_from(stage, d_list):
|
||||
ret = []
|
||||
for d in d_list:
|
||||
p = d.pop(OutputBase.PARAM_PATH)
|
||||
cache = d.pop(OutputBase.PARAM_CACHE, True)
|
||||
metric = d.pop(OutputBase.PARAM_METRIC, False)
|
||||
persist = d.pop(OutputBase.PARAM_PERSIST, False)
|
||||
tags = d.pop(OutputBase.PARAM_TAGS, None)
|
||||
p = d.pop(BaseOutput.PARAM_PATH)
|
||||
cache = d.pop(BaseOutput.PARAM_CACHE, True)
|
||||
metric = d.pop(BaseOutput.PARAM_METRIC, False)
|
||||
plot = d.pop(BaseOutput.PARAM_PLOT, False)
|
||||
persist = d.pop(BaseOutput.PARAM_PERSIST, False)
|
||||
ret.append(
|
||||
_get(
|
||||
stage,
|
||||
@@ -108,24 +118,67 @@ def loadd_from(stage, d_list):
|
||||
info=d,
|
||||
cache=cache,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
tags=tags,
|
||||
)
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
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,
|
||||
persist=persist,
|
||||
)
|
||||
def loads_from(
|
||||
stage, s_list, use_cache=True, metric=False, plot=False, persist=False
|
||||
):
|
||||
return [
|
||||
_get(
|
||||
stage,
|
||||
s,
|
||||
info={},
|
||||
cache=use_cache,
|
||||
metric=metric,
|
||||
plot=plot,
|
||||
persist=persist,
|
||||
)
|
||||
return ret
|
||||
for s in s_list
|
||||
]
|
||||
|
||||
|
||||
def _split_dict(d, keys):
|
||||
return project(d, keys), project(d, d.keys() - keys)
|
||||
|
||||
|
||||
def _merge_data(s_list):
|
||||
d = defaultdict(dict)
|
||||
for key in s_list:
|
||||
if isinstance(key, str):
|
||||
d[key].update({})
|
||||
continue
|
||||
if not isinstance(key, dict):
|
||||
raise ValueError(f"'{type(key).__name__}' not supported.")
|
||||
|
||||
for k, flags in key.items():
|
||||
if not isinstance(flags, dict):
|
||||
raise ValueError(
|
||||
f"Expected dict for '{k}', got: '{type(flags).__name__}'"
|
||||
)
|
||||
d[k].update(flags)
|
||||
return d
|
||||
|
||||
|
||||
@collecting
|
||||
def load_from_pipeline(stage, s_list, typ="outs"):
|
||||
if typ not in (stage.PARAM_OUTS, stage.PARAM_METRICS, stage.PARAM_PLOTS):
|
||||
raise ValueError(f"'{typ}' key is not allowed for pipeline files.")
|
||||
|
||||
metric = typ == stage.PARAM_METRICS
|
||||
plot = typ == stage.PARAM_PLOTS
|
||||
|
||||
d = _merge_data(s_list)
|
||||
|
||||
for path, flags in d.items():
|
||||
plt_d = {}
|
||||
if plot:
|
||||
from dvc.schema import PLOT_PROPS
|
||||
|
||||
plt_d, flags = _split_dict(flags, keys=PLOT_PROPS.keys())
|
||||
extra = project(flags, ["cache", "persist"])
|
||||
yield _get(stage, path, {}, plot=plt_d or plot, metric=metric, **extra)
|
||||
|
||||
+164
-101
@@ -1,52 +1,68 @@
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
import os
|
||||
from copy import copy
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from voluptuous import Any
|
||||
|
||||
import dvc.prompt as prompt
|
||||
from dvc.cache import NamedCache
|
||||
from dvc.exceptions import CollectCacheError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.remote.base import RemoteBASE
|
||||
from dvc.exceptions import (
|
||||
CollectCacheError,
|
||||
DvcException,
|
||||
RemoteCacheRequiredError,
|
||||
)
|
||||
|
||||
from ..tree.base import BaseRemoteTree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutputDoesNotExistError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = "output '{}' does not exist".format(path)
|
||||
msg = f"output '{path}' does not exist"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class OutputIsNotFileOrDirError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = "output '{}' is not a file or directory".format(path)
|
||||
msg = f"output '{path}' is not a file or directory"
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class OutputAlreadyTrackedError(DvcException):
|
||||
def __init__(self, path):
|
||||
msg = "output '{}' is already tracked by scm (e.g. git)".format(path)
|
||||
msg = f""" output '{path}' is already tracked by SCM (e.g. Git).
|
||||
You can remove it from Git, then add to DVC.
|
||||
To stop tracking from Git:
|
||||
git rm -r --cached '{path}'
|
||||
git commit -m "stop tracking {path}" """
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class OutputIsStageFileError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__("Stage file '{}' cannot be an output.".format(path))
|
||||
super().__init__(f"Stage file '{path}' cannot be an output.")
|
||||
|
||||
|
||||
class OutputBase(object):
|
||||
class BaseOutput:
|
||||
IS_DEPENDENCY = False
|
||||
|
||||
REMOTE = RemoteBASE
|
||||
TREE_CLS = BaseRemoteTree
|
||||
|
||||
PARAM_PATH = "path"
|
||||
PARAM_CACHE = "cache"
|
||||
PARAM_METRIC = "metric"
|
||||
PARAM_METRIC_TYPE = "type"
|
||||
PARAM_METRIC_XPATH = "xpath"
|
||||
PARAM_PLOT = "plot"
|
||||
PARAM_PLOT_TEMPLATE = "template"
|
||||
PARAM_PLOT_X = "x"
|
||||
PARAM_PLOT_Y = "y"
|
||||
PARAM_PLOT_X_LABEL = "x_label"
|
||||
PARAM_PLOT_Y_LABEL = "y_label"
|
||||
PARAM_PLOT_TITLE = "title"
|
||||
PARAM_PLOT_HEADER = "header"
|
||||
PARAM_PERSIST = "persist"
|
||||
|
||||
METRIC_SCHEMA = Any(
|
||||
@@ -58,8 +74,6 @@ class OutputBase(object):
|
||||
},
|
||||
)
|
||||
|
||||
PARAM_TAGS = "tags"
|
||||
|
||||
DoesNotExistError = OutputDoesNotExistError
|
||||
IsNotFileOrDirError = OutputIsNotFileOrDirError
|
||||
IsStageFileError = OutputIsStageFileError
|
||||
@@ -71,11 +85,11 @@ class OutputBase(object):
|
||||
stage,
|
||||
path,
|
||||
info=None,
|
||||
remote=None,
|
||||
tree=None,
|
||||
cache=True,
|
||||
metric=False,
|
||||
plot=False,
|
||||
persist=False,
|
||||
tags=None,
|
||||
):
|
||||
self._validate_output_path(path)
|
||||
# This output (and dependency) objects have too many paths/urls
|
||||
@@ -92,26 +106,24 @@ class OutputBase(object):
|
||||
self.repo = stage.repo if stage else None
|
||||
self.def_path = path
|
||||
self.info = info
|
||||
self.remote = remote or self.REMOTE(self.repo, {})
|
||||
if tree:
|
||||
self.tree = tree
|
||||
else:
|
||||
self.tree = self.TREE_CLS(self.repo, {})
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
self.plot = False if self.IS_DEPENDENCY else plot
|
||||
self.persist = persist
|
||||
self.tags = None if self.IS_DEPENDENCY else (tags or {})
|
||||
|
||||
self.path_info = self._parse_path(tree, path)
|
||||
if self.use_cache and self.cache is None:
|
||||
raise DvcException(
|
||||
"no cache location setup for '{}' outputs.".format(
|
||||
self.REMOTE.scheme
|
||||
)
|
||||
)
|
||||
raise RemoteCacheRequiredError(self.path_info)
|
||||
|
||||
self.path_info = self._parse_path(remote, path)
|
||||
|
||||
def _parse_path(self, remote, path):
|
||||
if remote:
|
||||
def _parse_path(self, tree, path):
|
||||
if tree:
|
||||
parsed = urlparse(path)
|
||||
return remote.path_info / parsed.path.lstrip("/")
|
||||
return self.REMOTE.path_cls(path)
|
||||
return tree.path_info / parsed.path.lstrip("/")
|
||||
return self.TREE_CLS.PATH_CLS(path)
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{def_path}'".format(
|
||||
@@ -123,7 +135,7 @@ class OutputBase(object):
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
return self.REMOTE.scheme
|
||||
return self.TREE_CLS.scheme
|
||||
|
||||
@property
|
||||
def is_in_repo(self):
|
||||
@@ -146,32 +158,46 @@ class OutputBase(object):
|
||||
|
||||
@classmethod
|
||||
def supported(cls, url):
|
||||
return cls.REMOTE.supported(url)
|
||||
return cls.TREE_CLS.supported(url)
|
||||
|
||||
@property
|
||||
def cache_path(self):
|
||||
return self.cache.checksum_to_path_info(self.checksum).url
|
||||
return self.cache.tree.hash_to_path_info(self.checksum).url
|
||||
|
||||
@property
|
||||
def checksum_type(self):
|
||||
return self.tree.PARAM_CHECKSUM
|
||||
|
||||
@property
|
||||
def checksum(self):
|
||||
return self.info.get(self.remote.PARAM_CHECKSUM)
|
||||
return self.info.get(self.tree.PARAM_CHECKSUM)
|
||||
|
||||
@checksum.setter
|
||||
def checksum(self, checksum):
|
||||
self.info[self.tree.PARAM_CHECKSUM] = checksum
|
||||
|
||||
def get_checksum(self):
|
||||
return self.tree.get_hash(self.path_info)
|
||||
|
||||
@property
|
||||
def is_dir_checksum(self):
|
||||
return self.remote.is_dir_checksum(self.checksum)
|
||||
return self.tree.is_dir_hash(self.checksum)
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
return self.remote.exists(self.path_info)
|
||||
return self.tree.exists(self.path_info)
|
||||
|
||||
def save_info(self):
|
||||
return self.tree.save_info(self.path_info)
|
||||
|
||||
def changed_checksum(self):
|
||||
return self.checksum != self.remote.get_checksum(self.path_info)
|
||||
return self.checksum != self.get_checksum()
|
||||
|
||||
def changed_cache(self):
|
||||
def changed_cache(self, filter_info=None):
|
||||
if not self.use_cache or not self.checksum:
|
||||
return True
|
||||
|
||||
return self.cache.changed_cache(self.checksum)
|
||||
return self.cache.changed_cache(self.checksum, filter_info=filter_info)
|
||||
|
||||
def status(self):
|
||||
if self.checksum and self.use_cache and self.changed_cache():
|
||||
@@ -195,13 +221,32 @@ class OutputBase(object):
|
||||
|
||||
@property
|
||||
def is_empty(self):
|
||||
return self.remote.is_empty(self.path_info)
|
||||
return self.tree.is_empty(self.path_info)
|
||||
|
||||
def isdir(self):
|
||||
return self.remote.isdir(self.path_info)
|
||||
return self.tree.isdir(self.path_info)
|
||||
|
||||
def isfile(self):
|
||||
return self.remote.isfile(self.path_info)
|
||||
return self.tree.isfile(self.path_info)
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
def ignore(self):
|
||||
if not self.use_scm_ignore:
|
||||
return
|
||||
|
||||
if self.repo.scm.is_tracked(self.fspath):
|
||||
raise OutputAlreadyTrackedError(self)
|
||||
|
||||
self.repo.scm.ignore(self.fspath)
|
||||
|
||||
def ignore_remove(self):
|
||||
if not self.use_scm_ignore:
|
||||
return
|
||||
|
||||
self.repo.scm.ignore_remove(self.fspath)
|
||||
|
||||
# pylint: enable=no-member
|
||||
|
||||
def save(self):
|
||||
if not self.exists:
|
||||
@@ -211,39 +256,32 @@ class OutputBase(object):
|
||||
raise self.IsNotFileOrDirError(self)
|
||||
|
||||
if self.is_empty:
|
||||
logger.warning("'{}' is empty.".format(self))
|
||||
logger.warning(f"'{self}' is empty.")
|
||||
|
||||
if self.use_scm_ignore:
|
||||
if self.repo.scm.is_tracked(self.fspath):
|
||||
raise OutputAlreadyTrackedError(self)
|
||||
self.ignore()
|
||||
|
||||
self.repo.scm.ignore(self.fspath)
|
||||
if self.metric or self.plot:
|
||||
self.verify_metric()
|
||||
|
||||
if not self.use_cache:
|
||||
self.info = self.remote.save_info(self.path_info)
|
||||
if self.metric:
|
||||
self.verify_metric()
|
||||
self.info = self.save_info()
|
||||
if not self.IS_DEPENDENCY:
|
||||
logger.info(
|
||||
"Output '{}' doesn't use cache. Skipping saving.".format(
|
||||
self
|
||||
)
|
||||
logger.debug(
|
||||
"Output '%s' doesn't use cache. Skipping saving.", self
|
||||
)
|
||||
return
|
||||
|
||||
assert not self.IS_DEPENDENCY
|
||||
|
||||
if not self.changed():
|
||||
logger.info(
|
||||
"Output '{}' didn't change. Skipping saving.".format(self)
|
||||
)
|
||||
logger.debug("Output '%s' didn't change. Skipping saving.", self)
|
||||
return
|
||||
|
||||
self.info = self.remote.save_info(self.path_info)
|
||||
self.info = self.save_info()
|
||||
|
||||
def commit(self):
|
||||
if self.use_cache:
|
||||
self.cache.save(self.path_info, self.info)
|
||||
self.cache.save(self.path_info, self.cache.tree, self.info)
|
||||
|
||||
def dumpd(self):
|
||||
ret = copy(self.info)
|
||||
@@ -252,7 +290,8 @@ class OutputBase(object):
|
||||
if self.IS_DEPENDENCY:
|
||||
return ret
|
||||
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
if not self.use_cache:
|
||||
ret[self.PARAM_CACHE] = self.use_cache
|
||||
|
||||
if isinstance(self.metric, dict):
|
||||
if (
|
||||
@@ -261,27 +300,27 @@ class OutputBase(object):
|
||||
):
|
||||
del self.metric[self.PARAM_METRIC_XPATH]
|
||||
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
if self.metric:
|
||||
ret[self.PARAM_METRIC] = self.metric
|
||||
|
||||
if self.tags:
|
||||
ret[self.PARAM_TAGS] = self.tags
|
||||
if self.plot:
|
||||
ret[self.PARAM_PLOT] = self.plot
|
||||
|
||||
if self.persist:
|
||||
ret[self.PARAM_PERSIST] = self.persist
|
||||
|
||||
return ret
|
||||
|
||||
def verify_metric(self):
|
||||
raise DvcException(
|
||||
"verify metric is not supported for {}".format(self.scheme)
|
||||
)
|
||||
raise DvcException(f"verify metric is not supported for {self.scheme}")
|
||||
|
||||
def download(self, to):
|
||||
self.remote.download(self.path_info, to.path_info)
|
||||
self.tree.download(self.path_info, to.path_info)
|
||||
|
||||
def checkout(
|
||||
self,
|
||||
force=False,
|
||||
progress_callback=None,
|
||||
tag=None,
|
||||
relink=False,
|
||||
filter_info=None,
|
||||
):
|
||||
@@ -292,14 +331,9 @@ class OutputBase(object):
|
||||
)
|
||||
return None
|
||||
|
||||
if tag:
|
||||
info = self.tags[tag]
|
||||
else:
|
||||
info = self.info
|
||||
|
||||
return self.cache.checkout(
|
||||
self.path_info,
|
||||
info,
|
||||
self.info,
|
||||
force=force,
|
||||
progress_callback=progress_callback,
|
||||
relink=relink,
|
||||
@@ -307,18 +341,19 @@ class OutputBase(object):
|
||||
)
|
||||
|
||||
def remove(self, ignore_remove=False):
|
||||
self.remote.remove(self.path_info)
|
||||
self.tree.remove(self.path_info)
|
||||
if self.scheme != "local":
|
||||
return
|
||||
|
||||
if ignore_remove and self.use_scm_ignore:
|
||||
self.repo.scm.ignore_remove(self.fspath)
|
||||
if ignore_remove:
|
||||
self.ignore_remove()
|
||||
|
||||
def move(self, out):
|
||||
# pylint: disable=no-member
|
||||
if self.scheme == "local" and self.use_scm_ignore:
|
||||
self.repo.scm.ignore_remove(self.fspath)
|
||||
|
||||
self.remote.move(self.path_info, out.path_info)
|
||||
self.tree.move(self.path_info, out.path_info)
|
||||
self.def_path = out.def_path
|
||||
self.path_info = out.path_info
|
||||
self.save()
|
||||
@@ -337,9 +372,20 @@ class OutputBase(object):
|
||||
|
||||
def unprotect(self):
|
||||
if self.exists:
|
||||
self.remote.unprotect(self.path_info)
|
||||
self.tree.unprotect(self.path_info)
|
||||
|
||||
def _collect_used_dir_cache(
|
||||
def get_dir_cache(self, **kwargs):
|
||||
if not self.is_dir_checksum:
|
||||
raise DvcException("cannot get dir cache for file checksum")
|
||||
if self.cache.changed_cache_file(self.checksum):
|
||||
self.repo.cloud.pull(
|
||||
NamedCache.make("local", self.checksum, str(self)),
|
||||
show_checksums=False,
|
||||
**kwargs,
|
||||
)
|
||||
return self.dir_cache
|
||||
|
||||
def collect_used_dir_cache(
|
||||
self, remote=None, force=False, jobs=None, filter_info=None
|
||||
):
|
||||
"""Get a list of `info`s related to the given directory.
|
||||
@@ -363,16 +409,10 @@ class OutputBase(object):
|
||||
|
||||
cache = NamedCache()
|
||||
|
||||
if self.cache.changed_cache_file(self.checksum):
|
||||
try:
|
||||
self.repo.cloud.pull(
|
||||
NamedCache.make("local", self.checksum, str(self)),
|
||||
jobs=jobs,
|
||||
remote=remote,
|
||||
show_checksums=False,
|
||||
)
|
||||
except DvcException:
|
||||
logger.debug("failed to pull cache for '{}'".format(self))
|
||||
try:
|
||||
self.get_dir_cache(jobs=jobs, remote=remote)
|
||||
except DvcException:
|
||||
logger.debug(f"failed to pull cache for '{self}'")
|
||||
|
||||
if self.cache.changed_cache_file(self.checksum):
|
||||
msg = (
|
||||
@@ -388,11 +428,21 @@ class OutputBase(object):
|
||||
else:
|
||||
return cache
|
||||
|
||||
path = str(self.path_info)
|
||||
filter_path = str(filter_info) if filter_info else None
|
||||
is_win = os.name == "nt"
|
||||
for entry in self.dir_cache:
|
||||
checksum = entry[self.remote.PARAM_CHECKSUM]
|
||||
info = self.path_info / entry[self.remote.PARAM_RELPATH]
|
||||
if not filter_info or info.isin_or_eq(filter_info):
|
||||
cache.add(self.scheme, checksum, str(info))
|
||||
checksum = entry[self.tree.PARAM_CHECKSUM]
|
||||
entry_relpath = entry[self.tree.PARAM_RELPATH]
|
||||
if is_win:
|
||||
entry_relpath = entry_relpath.replace("/", os.sep)
|
||||
entry_path = os.path.join(path, entry_relpath)
|
||||
if (
|
||||
not filter_path
|
||||
or entry_path == filter_path
|
||||
or entry_path.startswith(filter_path + os.sep)
|
||||
):
|
||||
cache.add(self.scheme, checksum, entry_path)
|
||||
|
||||
return cache
|
||||
|
||||
@@ -410,16 +460,27 @@ class OutputBase(object):
|
||||
|
||||
if self.stage.is_repo_import:
|
||||
cache = NamedCache()
|
||||
dep, = self.stage.deps
|
||||
(dep,) = self.stage.deps
|
||||
cache.external[dep.repo_pair].add(dep.def_path)
|
||||
return cache
|
||||
|
||||
if not self.info:
|
||||
logger.warning(
|
||||
"Output '{}'({}) is missing version info. Cache for it will "
|
||||
"not be collected. Use dvc repro to get your pipeline up to "
|
||||
"date.".format(self, self.stage)
|
||||
if not self.checksum:
|
||||
msg = (
|
||||
"Output '{}'({}) is missing version info. "
|
||||
"Cache for it will not be collected. "
|
||||
"Use `dvc repro` to get your pipeline up to date.".format(
|
||||
self, self.stage
|
||||
)
|
||||
)
|
||||
if self.exists:
|
||||
msg += (
|
||||
"\n"
|
||||
"You can also use `dvc commit {stage.addressing}` "
|
||||
"to associate existing '{out}' with {stage}.".format(
|
||||
out=self, stage=self.stage
|
||||
)
|
||||
)
|
||||
logger.warning(msg)
|
||||
return NamedCache()
|
||||
|
||||
ret = NamedCache.make(self.scheme, self.checksum, str(self))
|
||||
@@ -427,13 +488,15 @@ class OutputBase(object):
|
||||
if not self.is_dir_checksum:
|
||||
return ret
|
||||
|
||||
ret.update(self._collect_used_dir_cache(**kwargs))
|
||||
ret.add_child_cache(
|
||||
self.checksum, self.collect_used_dir_cache(**kwargs),
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def _validate_output_path(cls, path):
|
||||
from dvc.stage import Stage
|
||||
from dvc.dvcfile import is_valid_filename
|
||||
|
||||
if Stage.is_valid_filename(path):
|
||||
if is_valid_filename(path):
|
||||
raise cls.IsStageFileError(path)
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
from dvc.output.s3 import OutputS3
|
||||
from dvc.remote.gs import RemoteGS
|
||||
from dvc.output.s3 import S3Output
|
||||
|
||||
from ..tree.gs import GSRemoteTree
|
||||
|
||||
|
||||
class OutputGS(OutputS3):
|
||||
REMOTE = RemoteGS
|
||||
class GSOutput(S3Output):
|
||||
TREE_CLS = GSRemoteTree
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.remote.hdfs import RemoteHDFS
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..tree.hdfs import HDFSRemoteTree
|
||||
|
||||
|
||||
class OutputHDFS(OutputBase):
|
||||
REMOTE = RemoteHDFS
|
||||
class HDFSOutput(BaseOutput):
|
||||
TREE_CLS = HDFSRemoteTree
|
||||
|
||||
+27
-18
@@ -4,24 +4,32 @@ from urllib.parse import urlparse
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.istextfile import istextfile
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.remote.local import RemoteLOCAL
|
||||
from dvc.output.base import BaseOutput
|
||||
from dvc.scm.tree import is_working_tree
|
||||
from dvc.utils import relpath
|
||||
from dvc.compat import fspath_py35
|
||||
from dvc.utils.fs import path_isin
|
||||
|
||||
from ..tree.local import LocalRemoteTree
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutputLOCAL(OutputBase):
|
||||
REMOTE = RemoteLOCAL
|
||||
class LocalOutput(BaseOutput):
|
||||
TREE_CLS = LocalRemoteTree
|
||||
sep = os.sep
|
||||
|
||||
def _parse_path(self, remote, path):
|
||||
def __init__(self, stage, path, *args, **kwargs):
|
||||
if stage and path_isin(path, stage.repo.root_dir):
|
||||
path = relpath(path, stage.wdir)
|
||||
|
||||
super().__init__(stage, path, *args, **kwargs)
|
||||
if self.is_in_repo and self.repo and is_working_tree(self.repo.tree):
|
||||
self.tree = self.repo.tree
|
||||
|
||||
def _parse_path(self, tree, path):
|
||||
parsed = urlparse(path)
|
||||
if parsed.scheme == "remote":
|
||||
p = remote.path_info / parsed.path.lstrip("/")
|
||||
p = tree.path_info / parsed.path.lstrip("/")
|
||||
else:
|
||||
# NOTE: we can path either from command line or .dvc file,
|
||||
# so we should expect both posix and windows style paths.
|
||||
@@ -29,15 +37,15 @@ class OutputLOCAL(OutputBase):
|
||||
#
|
||||
# FIXME: if we have Windows path containing / or posix one with \
|
||||
# then we have #2059 bug and can't really handle that.
|
||||
p = self.REMOTE.path_cls(path)
|
||||
if not p.is_absolute():
|
||||
p = self.TREE_CLS.PATH_CLS(path)
|
||||
if self.stage and not p.is_absolute():
|
||||
p = self.stage.wdir / p
|
||||
|
||||
abs_p = os.path.abspath(os.path.normpath(fspath_py35(p)))
|
||||
return self.REMOTE.path_cls(abs_p)
|
||||
abs_p = os.path.abspath(os.path.normpath(p))
|
||||
return self.TREE_CLS.PATH_CLS(abs_p)
|
||||
|
||||
def __str__(self):
|
||||
if not self.is_in_repo:
|
||||
if not self.repo or not self.is_in_repo:
|
||||
return str(self.def_path)
|
||||
|
||||
cur_dir = os.getcwd()
|
||||
@@ -67,17 +75,18 @@ class OutputLOCAL(OutputBase):
|
||||
return ret
|
||||
|
||||
def verify_metric(self):
|
||||
if not self.metric:
|
||||
if not self.metric or self.plot:
|
||||
return
|
||||
|
||||
path = fspath_py35(self.path_info)
|
||||
path = os.fspath(self.path_info)
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
name = "metrics" if self.metric else "plot"
|
||||
if os.path.isdir(path):
|
||||
msg = "directory '{}' cannot be used as metrics."
|
||||
raise DvcException(msg.format(self.path_info))
|
||||
msg = "directory '{}' cannot be used as {}."
|
||||
raise IsADirectoryError(msg.format(self.path_info, name))
|
||||
|
||||
if not istextfile(path):
|
||||
msg = "binary file '{}' cannot be used as metrics."
|
||||
raise DvcException(msg.format(self.path_info))
|
||||
msg = "binary file '{}' cannot be used as {}."
|
||||
raise DvcException(msg.format(self.path_info, name))
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.remote.s3 import RemoteS3
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..tree.s3 import S3RemoteTree
|
||||
|
||||
|
||||
class OutputS3(OutputBase):
|
||||
REMOTE = RemoteS3
|
||||
class S3Output(BaseOutput):
|
||||
TREE_CLS = S3RemoteTree
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
from dvc.output.base import OutputBase
|
||||
from dvc.remote.ssh import RemoteSSH
|
||||
from dvc.output.base import BaseOutput
|
||||
|
||||
from ..tree.ssh import SSHRemoteTree
|
||||
|
||||
|
||||
class OutputSSH(OutputBase):
|
||||
REMOTE = RemoteSSH
|
||||
class SSHOutput(BaseOutput):
|
||||
TREE_CLS = SSHRemoteTree
|
||||
|
||||
+93
-19
@@ -1,6 +1,7 @@
|
||||
# pylint: disable=protected-access
|
||||
import os
|
||||
import posixpath
|
||||
import pathlib
|
||||
import posixpath
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import cached_property
|
||||
@@ -8,7 +9,7 @@ from funcy import cached_property
|
||||
from dvc.utils import relpath
|
||||
|
||||
|
||||
class _BasePath(object):
|
||||
class _BasePath:
|
||||
def overlaps(self, other):
|
||||
if isinstance(other, (str, bytes)):
|
||||
other = self.__class__(other)
|
||||
@@ -17,7 +18,7 @@ class _BasePath(object):
|
||||
return self.isin_or_eq(other) or other.isin(self)
|
||||
|
||||
def isin_or_eq(self, other):
|
||||
return self == other or self.isin(other)
|
||||
return self == other or self.isin(other) # pylint: disable=no-member
|
||||
|
||||
|
||||
class PathInfo(pathlib.PurePath, _BasePath):
|
||||
@@ -30,15 +31,14 @@ class PathInfo(pathlib.PurePath, _BasePath):
|
||||
def __new__(cls, *args):
|
||||
# Construct a proper subclass depending on current os
|
||||
if cls is PathInfo:
|
||||
cls = WindowsPathInfo if os.name == "nt" else PosixPathInfo
|
||||
cls = ( # pylint: disable=self-cls-assignment
|
||||
WindowsPathInfo if os.name == "nt" else PosixPathInfo
|
||||
)
|
||||
|
||||
return cls._from_parts(args)
|
||||
|
||||
@classmethod
|
||||
def from_posix(cls, s):
|
||||
return cls(PosixPathInfo(s))
|
||||
|
||||
def as_posix(self):
|
||||
f = self._flavour
|
||||
f = self._flavour # pylint: disable=no-member
|
||||
# Unlike original implementation [1] that uses `str()` we actually need
|
||||
# to use `fspath`, because we've overridden `__str__` method to return
|
||||
# relative paths, which will break original `as_posix`.
|
||||
@@ -54,7 +54,6 @@ class PathInfo(pathlib.PurePath, _BasePath):
|
||||
return "{}: '{}'".format(type(self).__name__, self)
|
||||
|
||||
# This permits passing it to file utils directly in Python 3.6+
|
||||
# With Python 2.7, Python 3.5+ we are stuck with path_info.fspath for now
|
||||
def __fspath__(self):
|
||||
return pathlib.PurePath.__str__(self)
|
||||
|
||||
@@ -92,7 +91,7 @@ class _URLPathInfo(PosixPathInfo):
|
||||
__unicode__ = __str__
|
||||
|
||||
|
||||
class _URLPathParents(object):
|
||||
class _URLPathParents:
|
||||
def __init__(self, src):
|
||||
self.src = src
|
||||
self._parents = self.src._path.parents
|
||||
@@ -104,7 +103,7 @@ class _URLPathParents(object):
|
||||
return self.src.replace(path=self._parents[idx])
|
||||
|
||||
def __repr__(self):
|
||||
return "<{}.parents>".format(self.src)
|
||||
return f"<{self.src}.parents>"
|
||||
|
||||
|
||||
class URLInfo(_BasePath):
|
||||
@@ -115,7 +114,7 @@ class URLInfo(_BasePath):
|
||||
assert not p.query and not p.params and not p.fragment
|
||||
assert p.password is None
|
||||
|
||||
self.fill_parts(p.scheme, p.hostname, p.username, p.port, p.path)
|
||||
self._fill_parts(p.scheme, p.hostname, p.username, p.port, p.path)
|
||||
|
||||
@classmethod
|
||||
def from_parts(
|
||||
@@ -124,13 +123,13 @@ class URLInfo(_BasePath):
|
||||
assert bool(host) ^ bool(netloc)
|
||||
|
||||
if netloc is not None:
|
||||
return cls("{}://{}{}".format(scheme, netloc, path))
|
||||
return cls(f"{scheme}://{netloc}{path}")
|
||||
|
||||
obj = cls.__new__(cls)
|
||||
obj.fill_parts(scheme, host, user, port, path)
|
||||
obj._fill_parts(scheme, host, user, port, path)
|
||||
return obj
|
||||
|
||||
def fill_parts(self, scheme, host, user, port, path):
|
||||
def _fill_parts(self, scheme, host, user, port, path):
|
||||
assert scheme != "remote"
|
||||
assert isinstance(path, (str, bytes, _URLPathInfo))
|
||||
|
||||
@@ -158,7 +157,7 @@ class URLInfo(_BasePath):
|
||||
|
||||
@cached_property
|
||||
def url(self):
|
||||
return "{}://{}{}".format(self.scheme, self.netloc, self._spath)
|
||||
return f"{self.scheme}://{self.netloc}{self._spath}"
|
||||
|
||||
def __str__(self):
|
||||
return self.url
|
||||
@@ -220,10 +219,10 @@ class URLInfo(_BasePath):
|
||||
if isinstance(other, (str, bytes)):
|
||||
other = self.__class__(other)
|
||||
if self.__class__ != other.__class__:
|
||||
msg = "'{}' has incompatible class with '{}'".format(self, other)
|
||||
msg = f"'{self}' has incompatible class with '{other}'"
|
||||
raise ValueError(msg)
|
||||
if self._base_parts != other._base_parts:
|
||||
msg = "'{}' does not start with '{}'".format(self, other)
|
||||
msg = f"'{self}' does not start with '{other}'"
|
||||
raise ValueError(msg)
|
||||
return self._path.relative_to(other._path)
|
||||
|
||||
@@ -241,3 +240,78 @@ class CloudURLInfo(URLInfo):
|
||||
@property
|
||||
def path(self):
|
||||
return self._spath.lstrip("/")
|
||||
|
||||
|
||||
class HTTPURLInfo(URLInfo):
|
||||
__hash__ = URLInfo.__hash__
|
||||
|
||||
def __init__(self, url):
|
||||
p = urlparse(url)
|
||||
stripped = p._replace(params=None, query=None, fragment=None)
|
||||
super().__init__(stripped.geturl())
|
||||
self.params = p.params
|
||||
self.query = p.query
|
||||
self.fragment = p.fragment
|
||||
|
||||
@classmethod
|
||||
def from_parts(
|
||||
cls,
|
||||
scheme=None,
|
||||
host=None,
|
||||
user=None,
|
||||
port=None,
|
||||
path="",
|
||||
netloc=None,
|
||||
params=None,
|
||||
query=None,
|
||||
fragment=None,
|
||||
): # pylint: disable=arguments-differ
|
||||
assert bool(host) ^ bool(netloc)
|
||||
|
||||
if netloc is not None:
|
||||
return cls(
|
||||
"{}://{}{}{}{}{}".format(
|
||||
scheme,
|
||||
netloc,
|
||||
path,
|
||||
(";" + params) if params else "",
|
||||
("?" + query) if query else "",
|
||||
("#" + fragment) if fragment else "",
|
||||
)
|
||||
)
|
||||
|
||||
obj = cls.__new__(cls)
|
||||
obj._fill_parts(scheme, host, user, port, path)
|
||||
obj.params = params
|
||||
obj.query = query
|
||||
obj.fragment = fragment
|
||||
return obj
|
||||
|
||||
@property
|
||||
def _extra_parts(self):
|
||||
return (self.params, self.query, self.fragment)
|
||||
|
||||
@property
|
||||
def parts(self):
|
||||
return self._base_parts + self._path.parts + self._extra_parts
|
||||
|
||||
@cached_property
|
||||
def url(self):
|
||||
return "{}://{}{}{}{}{}".format(
|
||||
self.scheme,
|
||||
self.netloc,
|
||||
self._spath,
|
||||
(";" + self.params) if self.params else "",
|
||||
("?" + self.query) if self.query else "",
|
||||
("#" + self.fragment) if self.fragment else "",
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, (str, bytes)):
|
||||
other = self.__class__(other)
|
||||
return (
|
||||
self.__class__ == other.__class__
|
||||
and self._base_parts == other._base_parts
|
||||
and self._path == other._path
|
||||
and self._extra_parts == other._extra_parts
|
||||
)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# Path Specification Pattern Math
|
||||
# Including changing base dir of path specification patterns and merging
|
||||
# of two path specification patterns with different base
|
||||
# All the operations follow the documents of `gitignore`
|
||||
import os
|
||||
|
||||
from pathspec.util import normalize_file
|
||||
|
||||
|
||||
def _not_ignore(rule):
|
||||
return (True, rule[1:]) if rule.startswith("!") else (False, rule)
|
||||
|
||||
|
||||
def _is_comment(rule):
|
||||
return rule.startswith("#")
|
||||
|
||||
|
||||
def _remove_slash(rule):
|
||||
if rule.startswith("\\"):
|
||||
return rule[1:]
|
||||
return rule
|
||||
|
||||
|
||||
def _match_all_level(rule):
|
||||
if rule[:-1].find("/") >= 0 and not rule.startswith("**/"):
|
||||
if rule.startswith("/"):
|
||||
rule = rule[1:]
|
||||
return False, rule
|
||||
if rule.startswith("**/"):
|
||||
rule = rule[3:]
|
||||
return True, rule
|
||||
|
||||
|
||||
def change_rule(rule, rel):
|
||||
rule = rule.strip()
|
||||
if _is_comment(rule):
|
||||
return rule
|
||||
not_ignore, rule = _not_ignore(rule)
|
||||
match_all, rule = _match_all_level(rule)
|
||||
rule = _remove_slash(rule)
|
||||
if not match_all:
|
||||
rule = f"/{rule}"
|
||||
else:
|
||||
rule = f"/**/{rule}"
|
||||
if not_ignore:
|
||||
rule = f"!/{rel}{rule}"
|
||||
else:
|
||||
rule = f"/{rel}{rule}"
|
||||
rule = normalize_file(rule)
|
||||
return rule
|
||||
|
||||
|
||||
def _change_dirname(dirname, pattern_list, new_dirname):
|
||||
if new_dirname == dirname:
|
||||
return pattern_list
|
||||
rel = os.path.relpath(dirname, new_dirname)
|
||||
if rel.startswith(".."):
|
||||
raise ValueError("change dirname can only change to parent path")
|
||||
|
||||
return [change_rule(rule, rel) for rule in pattern_list]
|
||||
|
||||
|
||||
def merge_patterns(prefix_a, pattern_a, prefix_b, pattern_b):
|
||||
"""
|
||||
Merge two path specification patterns.
|
||||
|
||||
This implementation merge two path specification patterns on different
|
||||
bases. It returns the longest common parent directory, and the patterns
|
||||
based on this new base directory.
|
||||
"""
|
||||
if not pattern_a:
|
||||
return prefix_b, pattern_b
|
||||
elif not pattern_b:
|
||||
return prefix_a, pattern_a
|
||||
|
||||
longest_common_dir = os.path.commonpath([prefix_a, prefix_b])
|
||||
new_pattern_a = _change_dirname(prefix_a, pattern_a, longest_common_dir)
|
||||
new_pattern_b = _change_dirname(prefix_b, pattern_b, longest_common_dir)
|
||||
|
||||
if len(prefix_a) < len(prefix_b):
|
||||
merged_pattern = new_pattern_a + new_pattern_b
|
||||
else:
|
||||
merged_pattern = new_pattern_b + new_pattern_a
|
||||
|
||||
return longest_common_dir, merged_pattern
|
||||
+50
-26
@@ -1,10 +1,9 @@
|
||||
"""Manages progress bars for dvc repo."""
|
||||
"""Manages progress bars for DVC repo."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from threading import RLock
|
||||
|
||||
from funcy import merge
|
||||
from tqdm import tqdm
|
||||
|
||||
from dvc.utils import env2bool
|
||||
@@ -20,18 +19,20 @@ class Tqdm(tqdm):
|
||||
|
||||
BAR_FMT_DEFAULT = (
|
||||
"{percentage:3.0f}% {desc}|{bar}|"
|
||||
"{n_fmt}/{total_fmt}"
|
||||
" [{elapsed}<{remaining}, {rate_fmt:>11}{postfix}]"
|
||||
"{postfix[info]}{n_fmt}/{total_fmt}"
|
||||
" [{elapsed}<{remaining}, {rate_fmt:>11}]"
|
||||
)
|
||||
# nested bars should have fixed bar widths to align nicely
|
||||
BAR_FMT_DEFAULT_NESTED = (
|
||||
"{percentage:3.0f}%|{bar:10}|{desc:{ncols_desc}.{ncols_desc}}"
|
||||
"{n_fmt}/{total_fmt}"
|
||||
" [{elapsed}<{remaining}, {rate_fmt:>11}{postfix}]"
|
||||
"{postfix[info]}{n_fmt}/{total_fmt}"
|
||||
" [{elapsed}<{remaining}, {rate_fmt:>11}]"
|
||||
)
|
||||
BAR_FMT_NOTOTAL = (
|
||||
"{desc:{ncols_desc}.{ncols_desc}}{n_fmt}"
|
||||
" [{elapsed}<??:??, {rate_fmt:>11}{postfix}]"
|
||||
"{desc}{bar:b}|{postfix[info]}{n_fmt} [{elapsed}, {rate_fmt:>11}]"
|
||||
)
|
||||
BYTES_DEFAULTS = dict(
|
||||
unit="B", unit_scale=True, unit_divisor=1024, miniters=1
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -44,6 +45,8 @@ class Tqdm(tqdm):
|
||||
bar_format=None,
|
||||
bytes=False, # pylint: disable=W0622
|
||||
file=None,
|
||||
total=None,
|
||||
postfix=None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
@@ -52,23 +55,21 @@ class Tqdm(tqdm):
|
||||
desc : persists after `close()`
|
||||
level : effective logging level for determining `disable`;
|
||||
used only if `disable` is unspecified
|
||||
disable : If (default: None), will be determined by logging level.
|
||||
disable : If (default: None) or False,
|
||||
will be determined by logging level.
|
||||
May be overridden to `True` due to non-TTY status.
|
||||
Skip override by specifying env var `DVC_IGNORE_ISATTY`.
|
||||
kwargs : anything accepted by `tqdm.tqdm()`
|
||||
"""
|
||||
kwargs = kwargs.copy()
|
||||
kwargs.setdefault("unit_scale", True)
|
||||
if bytes:
|
||||
bytes_defaults = dict(
|
||||
unit="B", unit_scale=True, unit_divisor=1024, miniters=1
|
||||
)
|
||||
kwargs = merge(bytes_defaults, kwargs)
|
||||
kwargs = {**self.BYTES_DEFAULTS, **kwargs}
|
||||
else:
|
||||
kwargs.setdefault("unit_scale", total > 999 if total else True)
|
||||
if file is None:
|
||||
file = sys.stderr
|
||||
self.desc_persist = desc
|
||||
# auto-disable based on `logger.level`
|
||||
if disable is None:
|
||||
if not disable:
|
||||
disable = logger.getEffectiveLevel() > level
|
||||
# auto-disable based on TTY
|
||||
if (
|
||||
@@ -84,8 +85,10 @@ class Tqdm(tqdm):
|
||||
desc=desc,
|
||||
bar_format="!",
|
||||
lock_args=(False,),
|
||||
total=total,
|
||||
**kwargs
|
||||
)
|
||||
self.postfix = postfix or {"info": ""}
|
||||
if bar_format is None:
|
||||
if self.__len__():
|
||||
self.bar_format = (
|
||||
@@ -99,11 +102,11 @@ class Tqdm(tqdm):
|
||||
self.bar_format = bar_format
|
||||
self.refresh()
|
||||
|
||||
def update_desc(self, desc, n=1):
|
||||
def update_msg(self, msg, n=1):
|
||||
"""
|
||||
Calls `set_description_str(desc)` and `update(n)`
|
||||
Sets `msg` as a postfix and calls `update(n)`.
|
||||
"""
|
||||
self.set_description_str(desc, refresh=False)
|
||||
self.postfix["info"] = " %s |" % msg
|
||||
self.update(n)
|
||||
|
||||
def update_to(self, current, total=None):
|
||||
@@ -111,9 +114,27 @@ class Tqdm(tqdm):
|
||||
self.total = total # pylint: disable=W0613,W0201
|
||||
self.update(current - self.n)
|
||||
|
||||
def wrap_fn(self, fn, callback=None):
|
||||
"""
|
||||
Returns a wrapped `fn` which calls `callback()` on each call.
|
||||
`callback` is `self.update` by default.
|
||||
"""
|
||||
if callback is None:
|
||||
callback = self.update
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
res = fn(*args, **kwargs)
|
||||
callback()
|
||||
return res
|
||||
|
||||
return wrapped
|
||||
|
||||
def close(self):
|
||||
if self.desc_persist is not None:
|
||||
self.set_description_str(self.desc_persist, refresh=False)
|
||||
self.postfix["info"] = ""
|
||||
# remove ETA (either unknown or zero); remove completed bar
|
||||
self.bar_format = self.bar_format.replace("<{remaining}", "").replace(
|
||||
"|{bar:10}|", " "
|
||||
)
|
||||
super().close()
|
||||
|
||||
@property
|
||||
@@ -121,12 +142,15 @@ class Tqdm(tqdm):
|
||||
"""inject `ncols_desc` to fill the display width (`ncols`)"""
|
||||
d = super().format_dict
|
||||
ncols = d["ncols"] or 80
|
||||
ncols_desc = ncols - len(self.format_meter(ncols_desc=1, **d)) + 1
|
||||
ncols_desc = max(ncols_desc, 0)
|
||||
if ncols_desc:
|
||||
d["ncols_desc"] = ncols_desc
|
||||
# assumes `bar_format` has max one of ("ncols_desc" & "ncols_info")
|
||||
ncols_left = (
|
||||
ncols - len(self.format_meter(ncols_desc=1, ncols_info=1, **d)) + 1
|
||||
)
|
||||
ncols_left = max(ncols_left, 0)
|
||||
if ncols_left:
|
||||
d["ncols_desc"] = d["ncols_info"] = ncols_left
|
||||
else:
|
||||
# work-around for zero-width description
|
||||
d["ncols_desc"] = 1
|
||||
d["ncols_desc"] = d["ncols_info"] = 1
|
||||
d["prefix"] = ""
|
||||
return d
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user