mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-02 15:28:30 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73f2fa8c93 | ||
|
|
b652670cbf | ||
|
|
6de3e734ed | ||
|
|
4b2bec89ec | ||
|
|
6c0bc9563c | ||
|
|
f964a44e7c | ||
|
|
464620806c | ||
|
|
4244e5a475 | ||
|
|
0c41a25bbe | ||
|
|
26f435a433 | ||
|
|
966dc8a1ca | ||
|
|
6d96ecb376 | ||
|
|
9e57d1c32d | ||
|
|
c08655ac38 | ||
|
|
4f16852005 | ||
|
|
7bef6fd64f | ||
|
|
422d883e18 | ||
|
|
48004b1340 | ||
|
|
2ae421cea2 | ||
|
|
812fde9891 | ||
|
|
4ed4766692 | ||
|
|
9943cd43b1 | ||
|
|
7d3f35e6ab | ||
|
|
4de261f717 | ||
|
|
db75878bef | ||
|
|
7f5c53c10e | ||
|
|
259d590527 | ||
|
|
709bfe598f | ||
|
|
7e4f443002 | ||
|
|
2a3a93314b | ||
|
|
a1b146669c | ||
|
|
b8478a8b04 | ||
|
|
0ae9130052 | ||
|
|
af34f5a0f1 | ||
|
|
e9a7e0657d | ||
|
|
8abda93e3e | ||
|
|
53255c22b3 | ||
|
|
6da15629c4 | ||
|
|
5cfe61928e | ||
|
|
32467ada32 | ||
|
|
5cbaa8d98e | ||
|
|
4762c38ff7 | ||
|
|
66acbc892a |
@@ -10,7 +10,9 @@ jobs:
|
||||
deb:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -39,7 +41,9 @@ jobs:
|
||||
rpm:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -68,7 +72,9 @@ jobs:
|
||||
osxpkg:
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -100,7 +106,9 @@ jobs:
|
||||
exe:
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -128,7 +136,9 @@ jobs:
|
||||
pip:
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -136,7 +146,9 @@ jobs:
|
||||
- name: Install
|
||||
run: pip install ".[all,tests]"
|
||||
- name: Build packages
|
||||
run: ./scripts/build_package.sh
|
||||
run: |
|
||||
pip install -U pip
|
||||
./scripts/build_package.sh
|
||||
- name: Publish packages to PyPI
|
||||
if: github.event_name == 'release'
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
|
||||
@@ -20,7 +20,9 @@ jobs:
|
||||
uses: styfle/[email protected]
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -28,7 +30,7 @@ jobs:
|
||||
- name: Install requirements
|
||||
run: |
|
||||
pip install wheel
|
||||
pip install ".[all,tests,terraform]" Pygments collective.checkdocs pre-commit
|
||||
pip install ".[all,tests,terraform]" pre-commit
|
||||
- name: install temporary dependencies
|
||||
run: |
|
||||
pip install git+https://github.com/isidentical/gdrivefs@service-account
|
||||
@@ -43,13 +45,15 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-20.04, windows-latest, macos-latest]
|
||||
pyv: ["3.6", "3.7", "3.8", "3.9", "3.10"]
|
||||
pyv: ["3.7", "3.8", "3.9", "3.10"]
|
||||
exclude:
|
||||
# no wheels for pygit2 yet
|
||||
- os: windows-latest
|
||||
pyv: "3.10"
|
||||
steps:
|
||||
- uses: actions/[email protected].4
|
||||
- uses: actions/[email protected].5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
@@ -105,7 +109,7 @@ jobs:
|
||||
run: >-
|
||||
python -m tests -n=4
|
||||
--cov-report=xml --cov-report=term
|
||||
--all ${{ env.extra_test_args }}
|
||||
--enable-ssh ${{ env.extra_test_args }}
|
||||
- name: upload coverage report
|
||||
uses: codecov/[email protected]
|
||||
with:
|
||||
|
||||
@@ -13,6 +13,7 @@ build/
|
||||
dist/
|
||||
|
||||
*.egg-info/
|
||||
.eggs
|
||||
|
||||
hooks/*.pyc
|
||||
|
||||
@@ -38,3 +39,4 @@ scripts/ci/gcp-creds.json
|
||||
.gcp-creds.json
|
||||
|
||||
*~
|
||||
/dvc/_dvc_version.py
|
||||
|
||||
@@ -7,6 +7,11 @@ repos:
|
||||
language_version: python3
|
||||
repo: https://github.com/ambv/black
|
||||
rev: 21.9b0
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- hooks:
|
||||
- id: codespell
|
||||
args:
|
||||
@@ -39,7 +44,7 @@ repos:
|
||||
- types-tabulate
|
||||
- types-toml
|
||||
repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: 'v0.910'
|
||||
rev: 'v0.910-1'
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
@@ -54,7 +59,7 @@ repos:
|
||||
id: beautysh
|
||||
language_version: python3
|
||||
repo: https://github.com/lovesegfault/beautysh
|
||||
rev: v6.1.0
|
||||
rev: v6.2.1
|
||||
- hooks:
|
||||
- args:
|
||||
- git-hook
|
||||
|
||||
@@ -1,615 +0,0 @@
|
||||
[MASTER]
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-whitelist=pygit2
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not
|
||||
# paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the blacklist. The
|
||||
# regex matches against base names, not paths.
|
||||
ignore-patterns=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
init-hook="import sys;
|
||||
from pathlib import Path;
|
||||
from pylint.config import find_pylintrc;
|
||||
|
||||
tests_folder = Path(find_pylintrc()).parent / 'tests'
|
||||
sys.path.append(str(tests_folder));
|
||||
"
|
||||
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_pytest,pylint_plugin_disable
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Specify a configuration file.
|
||||
#rcfile=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||
confidence=
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then reenable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=format,
|
||||
# ^ most of them are covered by flake8,
|
||||
# and are conventions rather than warnings or errors
|
||||
refactoring, # will be noisy if it starts giving suggestions
|
||||
spelling,
|
||||
# design is too-few-* and too-many-* checks, too-much-* for us right now
|
||||
design,
|
||||
# following were generated by default
|
||||
# most of them are redundant Py3 checks for Py2+3 codebase
|
||||
print-statement,
|
||||
parameter-unpacking,
|
||||
unpacking-in-except,
|
||||
old-raise-syntax,
|
||||
backtick,
|
||||
long-suffix,
|
||||
old-ne-operator,
|
||||
old-octal-literal,
|
||||
import-star-module-level,
|
||||
non-ascii-bytes-literal,
|
||||
raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
apply-builtin,
|
||||
basestring-builtin,
|
||||
buffer-builtin,
|
||||
cmp-builtin,
|
||||
coerce-builtin,
|
||||
execfile-builtin,
|
||||
file-builtin,
|
||||
long-builtin,
|
||||
raw_input-builtin,
|
||||
reduce-builtin,
|
||||
standarderror-builtin,
|
||||
unicode-builtin,
|
||||
xrange-builtin,
|
||||
coerce-method,
|
||||
delslice-method,
|
||||
getslice-method,
|
||||
setslice-method,
|
||||
no-absolute-import,
|
||||
old-division,
|
||||
dict-iter-method,
|
||||
dict-view-method,
|
||||
next-method-called,
|
||||
metaclass-assignment,
|
||||
indexing-exception,
|
||||
raising-string,
|
||||
reload-builtin,
|
||||
oct-method,
|
||||
hex-method,
|
||||
nonzero-method,
|
||||
cmp-method,
|
||||
input-builtin,
|
||||
round-builtin,
|
||||
intern-builtin,
|
||||
unichr-builtin,
|
||||
map-builtin-not-iterating,
|
||||
zip-builtin-not-iterating,
|
||||
range-builtin-not-iterating,
|
||||
filter-builtin-not-iterating,
|
||||
using-cmp-argument,
|
||||
eq-without-hash,
|
||||
div-method,
|
||||
idiv-method,
|
||||
rdiv-method,
|
||||
exception-message-attribute,
|
||||
invalid-str-codec,
|
||||
sys-max-int,
|
||||
bad-python3-import,
|
||||
deprecated-string-function,
|
||||
deprecated-str-translate-call,
|
||||
deprecated-itertools-function,
|
||||
deprecated-types-field,
|
||||
next-method-defined,
|
||||
dict-items-not-iterating,
|
||||
dict-keys-not-iterating,
|
||||
dict-values-not-iterating,
|
||||
deprecated-operator-function,
|
||||
deprecated-urllib-function,
|
||||
xreadlines-attribute,
|
||||
deprecated-sys-function,
|
||||
exception-escape,
|
||||
comprehension-escape,
|
||||
# custom disables start here
|
||||
no-self-use,
|
||||
invalid-name,
|
||||
# already a check enabled for `wildcard-import`
|
||||
unused-wildcard-import,
|
||||
misplaced-comparison-constant,
|
||||
# probably soon: https://github.com/iterative/dvc/issues/1843
|
||||
logging-format-interpolation,
|
||||
logging-fstring-interpolation,
|
||||
# we don't have proper docstrings yet
|
||||
# even if we do, we'll be doing it gradually
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
missing-class-docstring,
|
||||
duplicate-code,
|
||||
# too noisy, and detects issues even when import is inside a function
|
||||
# see: https://github.com/PyCQA/pylint/issues/850
|
||||
cyclic-import,
|
||||
# isort should cover these:
|
||||
wrong-import-order,
|
||||
wrong-import-position,
|
||||
ungrouped-imports,
|
||||
multiple-imports,
|
||||
# we use it to speedup/optimize
|
||||
import-outside-toplevel,
|
||||
fixme,
|
||||
# we use cause of the exception for error message output
|
||||
# so some of the times we don't use `raise from` to not show the cause
|
||||
# of the error
|
||||
raise-missing-from,
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member,
|
||||
no-else-return,
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a note less than 10 (10 is the highest
|
||||
# note). You have access to the variables errors warning, statement which
|
||||
# respectively contain the number of errors / warnings messages and the total
|
||||
# number of statements analyzed. This is used by the global evaluation report
|
||||
# (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
#msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
output-format=text
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# Format style used to check logging format string. `old` means using %
|
||||
# formatting, while `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||
# install python-enchant package..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to indicated private dictionary in
|
||||
# --spelling-private-dict-file option instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=hdfs.*,pytest.lazy_fixture,logging.TRACE,logger.trace,sys.getwindowsversion
|
||||
|
||||
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||
ignore-mixin-members=yes
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,Dvcfile
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=azure
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
signature-mutators=funcy.decorators.decorator,
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_|args|kwargs
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=79
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||
# `empty-line` allows space-only lines.
|
||||
no-space-check=trailing-comma,
|
||||
dict-separator
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_,
|
||||
s3,
|
||||
gs,
|
||||
x, y,
|
||||
x0, y0,
|
||||
x1, y1,
|
||||
dx, dy
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Ignore comments when computing similarities.
|
||||
ignore-comments=yes
|
||||
|
||||
# Ignore docstrings when computing similarities.
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Ignore imports when computing similarities.
|
||||
ignore-imports=no
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=cls
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse,tkinter.tix
|
||||
|
||||
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||
# given file (report RP0402 must not be disabled).
|
||||
import-graph=
|
||||
|
||||
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||
# not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when being caught. Defaults to
|
||||
# "Exception".
|
||||
overgeneral-exceptions=Exception
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
include fastentrypoints.py
|
||||
include LICENSE
|
||||
recursive-include requirements *.txt
|
||||
recursive-include dvc/repo/plots/templates *.json
|
||||
|
||||
+3
-3
@@ -68,7 +68,7 @@ def send(path):
|
||||
url = "https://analytics.dvc.org"
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
with open(path) as fobj:
|
||||
with open(path, encoding="utf-8") as fobj:
|
||||
report = json.load(fobj)
|
||||
|
||||
report.update(_runtime_info())
|
||||
@@ -173,13 +173,13 @@ def _find_or_create_user_id():
|
||||
try:
|
||||
with Lock(lockfile):
|
||||
try:
|
||||
with open(fname) as fobj:
|
||||
with open(fname, encoding="utf-8") as fobj:
|
||||
user_id = json.load(fobj)["user_id"]
|
||||
|
||||
except (FileNotFoundError, ValueError, KeyError):
|
||||
user_id = str(uuid.uuid4())
|
||||
|
||||
with open(fname, "w") as fobj:
|
||||
with open(fname, "w", encoding="utf-8") as fobj:
|
||||
json.dump({"user_id": user_id}, fobj)
|
||||
|
||||
return user_id
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@ def make_checkpoint():
|
||||
root_dir, Repo.DVC_DIR, "tmp", CheckpointTask.SIGNAL_FILE
|
||||
)
|
||||
|
||||
with builtins.open(signal_file, "w") as fobj:
|
||||
with builtins.open(signal_file, "w", encoding="utf-8") as fobj:
|
||||
# NOTE: force flushing/writing empty file to disk, otherwise when
|
||||
# run in certain contexts (pytest) file may not actually be written
|
||||
fobj.write("")
|
||||
|
||||
@@ -382,7 +382,12 @@ def baseline_styler(typ):
|
||||
|
||||
|
||||
def show_experiments(
|
||||
all_experiments, pager=True, no_timestamp=False, csv=False, **kwargs
|
||||
all_experiments,
|
||||
pager=True,
|
||||
no_timestamp=False,
|
||||
csv=False,
|
||||
markdown=False,
|
||||
**kwargs,
|
||||
):
|
||||
from funcy.seqs import flatten as flatten_list
|
||||
|
||||
@@ -473,6 +478,7 @@ def show_experiments(
|
||||
header_styles=styles,
|
||||
row_styles=row_styles,
|
||||
csv=csv,
|
||||
markdown=markdown,
|
||||
)
|
||||
|
||||
|
||||
@@ -530,6 +536,7 @@ class CmdExperimentsShow(CmdBase):
|
||||
iso=iso,
|
||||
pager=not self.args.no_pager,
|
||||
csv=self.args.csv,
|
||||
markdown=self.args.markdown,
|
||||
)
|
||||
return 0
|
||||
|
||||
@@ -579,6 +586,8 @@ class CmdExperimentsDiff(CmdBase):
|
||||
old=self.args.old,
|
||||
on_empty_diff="diff not supported",
|
||||
precision=precision if key == "metrics" else None,
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
)
|
||||
|
||||
return 0
|
||||
@@ -956,7 +965,10 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--sort-order",
|
||||
help="Sort order to use with --sort-by.",
|
||||
help=(
|
||||
"Sort order to use with --sort-by."
|
||||
" Defaults to ascending ('asc')."
|
||||
),
|
||||
choices=("asc", "desc"),
|
||||
default="asc",
|
||||
)
|
||||
@@ -986,6 +998,14 @@ def add_parser(subparsers, parent_parser):
|
||||
default=False,
|
||||
help="Print output in csv format instead of a human-readable table.",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--md",
|
||||
"--show-md",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="markdown",
|
||||
help="Show tabulated output in the Markdown format (GFM).",
|
||||
)
|
||||
experiments_show_parser.add_argument(
|
||||
"--precision",
|
||||
type=int,
|
||||
@@ -1067,7 +1087,7 @@ def add_parser(subparsers, parent_parser):
|
||||
"--old",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Show old metric/param value.",
|
||||
help="Show old ('a_rev') metric/param value.",
|
||||
)
|
||||
experiments_diff_parser.add_argument(
|
||||
"--no-path",
|
||||
|
||||
@@ -3,6 +3,7 @@ import argparse
|
||||
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
|
||||
from dvc.command.config import CmdConfig
|
||||
from dvc.config import ConfigError
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.ui import ui
|
||||
from dvc.utils import format_link
|
||||
|
||||
@@ -104,6 +105,54 @@ class CmdMachineModify(CmdMachineConfig):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineRename(CmdBase):
|
||||
def _check_exists(self, conf):
|
||||
if self.args.name not in conf["machine"]:
|
||||
raise ConfigError(f"machine '{self.args.name}' doesn't exist.")
|
||||
|
||||
def _rename_default(self, conf):
|
||||
if conf["core"].get("machine") == self.args.name:
|
||||
conf["core"]["machine"] = self.args.new
|
||||
|
||||
def _check_before_rename(self):
|
||||
from dvc.machine import validate_name
|
||||
|
||||
validate_name(self.args.new)
|
||||
|
||||
all_config = self.config.load_config_to_level(None)
|
||||
if self.args.new in all_config.get("machine", {}):
|
||||
raise ConfigError(
|
||||
"Rename failed. Machine '{}' already exists.".format(
|
||||
self.args.new
|
||||
)
|
||||
)
|
||||
ui.write(f"Rename machine '{self.args.name}' to '{self.args.new}'.")
|
||||
|
||||
def run(self):
|
||||
|
||||
self._check_before_rename()
|
||||
|
||||
with self.config.edit(self.args.level) as conf:
|
||||
self._check_exists(conf)
|
||||
conf["machine"][self.args.new] = conf["machine"][self.args.name]
|
||||
try:
|
||||
self.repo.machine.rename(self.args.name, self.args.new)
|
||||
except DvcException as error:
|
||||
del conf["machine"][self.args.new]
|
||||
raise ConfigError("terraform rename failed") from error
|
||||
del conf["machine"][self.args.name]
|
||||
self._rename_default(conf)
|
||||
|
||||
up_to_level = self.args.level or "repo"
|
||||
for level in reversed(self.config.LEVELS):
|
||||
if level == up_to_level:
|
||||
break
|
||||
with self.config.edit(level) as level_conf:
|
||||
self._rename_default(level_conf)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineDefault(CmdMachineConfig):
|
||||
def run(self):
|
||||
if self.args.name is None and not self.args.unset:
|
||||
@@ -143,6 +192,22 @@ class CmdMachineCreate(CmdBase):
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineStatus(CmdBase):
|
||||
SHOWN_FIELD = ["name", "cloud", "instance_hdd_size", "instance_ip"]
|
||||
|
||||
def run(self):
|
||||
if self.repo.machine is None:
|
||||
raise MachineDisabledError
|
||||
|
||||
all_status = self.repo.machine.status(self.args.name)
|
||||
for i, status in enumerate(all_status, start=1):
|
||||
ui.write(f"instance_num_{i}:")
|
||||
for field in self.SHOWN_FIELD:
|
||||
value = status.get(field, None)
|
||||
ui.write(f"\t{field:20}: {value}")
|
||||
return 0
|
||||
|
||||
|
||||
class CmdMachineDestroy(CmdBase):
|
||||
def run(self):
|
||||
if self.repo.machine is None:
|
||||
@@ -280,6 +345,18 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
machine_modify_parser.set_defaults(func=CmdMachineModify)
|
||||
|
||||
machine_RENAME_HELP = "Rename a machine "
|
||||
machine_rename_parser = machine_subparsers.add_parser(
|
||||
"rename",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_RENAME_HELP, "remote/rename"),
|
||||
help=machine_RENAME_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_rename_parser.add_argument("name", help="Machine to be renamed")
|
||||
machine_rename_parser.add_argument("new", help="New name of the machine")
|
||||
machine_rename_parser.set_defaults(func=CmdMachineRename)
|
||||
|
||||
machine_REMOVE_HELP = "Remove an machine."
|
||||
machine_remove_parser = machine_subparsers.add_parser(
|
||||
"remove",
|
||||
@@ -306,6 +383,19 @@ def add_parser(subparsers, parent_parser):
|
||||
)
|
||||
machine_create_parser.set_defaults(func=CmdMachineCreate)
|
||||
|
||||
machine_STATUS_HELP = "List the status of a running machine."
|
||||
machine_status_parser = machine_subparsers.add_parser(
|
||||
"status",
|
||||
parents=[parent_config_parser, parent_parser],
|
||||
description=append_doc_link(machine_STATUS_HELP, "machine/status"),
|
||||
help=machine_STATUS_HELP,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
machine_status_parser.add_argument(
|
||||
"name", help="Name of the running machine."
|
||||
)
|
||||
machine_status_parser.set_defaults(func=CmdMachineStatus)
|
||||
|
||||
machine_DESTROY_HELP = "Destroy an machine instance."
|
||||
machine_destroy_parser = machine_subparsers.add_parser(
|
||||
"destroy",
|
||||
|
||||
@@ -75,6 +75,8 @@ class CmdMetricsDiff(CmdMetricsBase):
|
||||
no_path=self.args.no_path,
|
||||
precision=self.args.precision or DEFAULT_PRECISION,
|
||||
round_digits=True,
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -36,6 +36,8 @@ class CmdParamsDiff(CmdBase):
|
||||
markdown=self.args.markdown,
|
||||
no_path=self.args.no_path,
|
||||
show_changes=False,
|
||||
a_rev=self.args.a_rev,
|
||||
b_rev=self.args.b_rev,
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
@@ -183,7 +183,10 @@ def add_parser(subparsers, parent_parser):
|
||||
_add_output_arguments(plots_diff_parser)
|
||||
plots_diff_parser.set_defaults(func=CmdPlotsDiff)
|
||||
|
||||
PLOTS_MODIFY_HELP = "Modify display properties of plot metrics files."
|
||||
PLOTS_MODIFY_HELP = (
|
||||
"Modify display properties of data-series plots "
|
||||
"(has no effect on image-type plots)."
|
||||
)
|
||||
plots_modify_parser = plots_subparsers.add_parser(
|
||||
"modify",
|
||||
parents=[parent_parser],
|
||||
|
||||
+11
-3
@@ -211,8 +211,12 @@ def diff_table(
|
||||
precision: int = None,
|
||||
round_digits: bool = False,
|
||||
on_empty_diff: str = None,
|
||||
a_rev: str = None,
|
||||
b_rev: str = None,
|
||||
) -> TabularData:
|
||||
headers: List[str] = ["Path", title, "Old", "New", "Change"]
|
||||
a_rev = a_rev or "HEAD"
|
||||
b_rev = b_rev or "workspace"
|
||||
headers: List[str] = ["Path", title, a_rev, b_rev, "Change"]
|
||||
fill_value = "-"
|
||||
td = TabularData(headers, fill_value=fill_value)
|
||||
|
||||
@@ -240,8 +244,8 @@ def diff_table(
|
||||
td.drop("Change")
|
||||
|
||||
if not old:
|
||||
td.drop("Old")
|
||||
td.rename("New", "Value")
|
||||
td.drop(a_rev)
|
||||
td.rename(b_rev, "Value")
|
||||
|
||||
return td
|
||||
|
||||
@@ -256,6 +260,8 @@ def show_diff(
|
||||
round_digits: bool = False,
|
||||
on_empty_diff: str = None,
|
||||
markdown: bool = False,
|
||||
a_rev: str = None,
|
||||
b_rev: str = None,
|
||||
) -> None:
|
||||
td = diff_table(
|
||||
diff,
|
||||
@@ -266,6 +272,8 @@ def show_diff(
|
||||
precision=precision,
|
||||
round_digits=round_digits,
|
||||
on_empty_diff=on_empty_diff,
|
||||
a_rev=a_rev,
|
||||
b_rev=b_rev,
|
||||
)
|
||||
td.render(markdown=markdown)
|
||||
|
||||
|
||||
+1
-1
@@ -141,7 +141,7 @@ class Config(dict):
|
||||
dvc.config.Config: config object.
|
||||
"""
|
||||
config_file = os.path.join(dvc_dir, Config.CONFIG)
|
||||
open(config_file, "w+").close()
|
||||
open(config_file, "w+", encoding="utf-8").close()
|
||||
return Config(dvc_dir)
|
||||
|
||||
def load(self, validate=True, config=None):
|
||||
|
||||
+4
-5
@@ -12,8 +12,6 @@ from dvc.utils import fix_env, is_binary
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CREATE_NO_WINDOW = 0x08000000 # only available since Python 3.7 in subprocess.
|
||||
|
||||
|
||||
def _popen(cmd, **kwargs):
|
||||
prefix = [sys.executable]
|
||||
@@ -28,6 +26,7 @@ def _popen(cmd, **kwargs):
|
||||
def _spawn_windows(cmd, env):
|
||||
from subprocess import (
|
||||
CREATE_NEW_PROCESS_GROUP,
|
||||
CREATE_NO_WINDOW,
|
||||
STARTF_USESHOWWINDOW,
|
||||
STARTUPINFO,
|
||||
)
|
||||
@@ -49,17 +48,17 @@ def _spawn_posix(cmd, env):
|
||||
# with PyInstaller has trouble with SystemExit exception and throws
|
||||
# errors such as "[26338] Failed to execute script __main__"
|
||||
try:
|
||||
pid = os.fork()
|
||||
pid = os.fork() # pylint: disable=no-member
|
||||
if pid > 0:
|
||||
return
|
||||
except OSError:
|
||||
logger.exception("failed at first fork")
|
||||
os._exit(1) # pylint: disable=protected-access
|
||||
|
||||
os.setsid()
|
||||
os.setsid() # pylint: disable=no-member
|
||||
|
||||
try:
|
||||
pid = os.fork()
|
||||
pid = os.fork() # pylint: disable=no-member
|
||||
if pid > 0:
|
||||
os._exit(0) # pylint: disable=protected-access
|
||||
except OSError:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -10,6 +11,8 @@ from dvc.utils.serialize import LOADERS, ParseError
|
||||
|
||||
from .base import Dependency
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MissingParamsError(DvcException):
|
||||
pass
|
||||
@@ -133,3 +136,20 @@ class ParamsDependency(Dependency):
|
||||
)
|
||||
|
||||
return HashInfo(self.PARAM_PARAMS, info)
|
||||
|
||||
def save(self):
|
||||
if not self.exists:
|
||||
raise self.DoesNotExistError(self)
|
||||
|
||||
if not self.isfile and not self.isdir:
|
||||
raise self.IsNotFileOrDirError(self)
|
||||
|
||||
if self.is_empty:
|
||||
logger.warning(f"'{self}' is empty.")
|
||||
|
||||
self.ignore()
|
||||
|
||||
if self.metric or self.plot:
|
||||
self.verify_metric()
|
||||
|
||||
self.hash_info = self.get_hash()
|
||||
|
||||
@@ -132,7 +132,7 @@ class RepoDependency(Dependency):
|
||||
pass
|
||||
|
||||
try:
|
||||
staging, staged_obj = stage(
|
||||
staging, _, staged_obj = stage(
|
||||
local_odb,
|
||||
path_info,
|
||||
repo.repo_fs,
|
||||
@@ -148,9 +148,7 @@ class RepoDependency(Dependency):
|
||||
self._staged_objs[rev] = staged_obj
|
||||
used_obj_ids[staging].add(staged_obj.hash_info)
|
||||
if isinstance(staged_obj, Tree):
|
||||
used_obj_ids[staging].update(
|
||||
entry.hash_info for _, entry in staged_obj
|
||||
)
|
||||
used_obj_ids[staging].update(oid for _, _, oid in staged_obj)
|
||||
return used_obj_ids, staged_obj
|
||||
|
||||
def _check_circular_import(self, odb, obj_ids):
|
||||
@@ -166,7 +164,7 @@ class RepoDependency(Dependency):
|
||||
for hash_info in obj_ids:
|
||||
if hash_info.isdir:
|
||||
tree = Tree.load(odb, hash_info)
|
||||
yield from (odb.get(entry.hash_info) for _, entry in tree)
|
||||
yield from (odb.get(oid) for _, _, oid in tree)
|
||||
else:
|
||||
yield odb.get(hash_info)
|
||||
|
||||
|
||||
+22
-19
@@ -58,9 +58,9 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
out.get_dir_cache(remote=remote)
|
||||
if out.obj is None:
|
||||
raise FileNotFoundError
|
||||
obj = out.obj.trie.get(key)
|
||||
if obj:
|
||||
return obj.hash_info
|
||||
(_, oid) = out.obj.trie.get(key) or (None, None)
|
||||
if oid:
|
||||
return oid
|
||||
raise FileNotFoundError
|
||||
|
||||
def _get_fs_path(self, path: PathInfo, remote=None):
|
||||
@@ -154,7 +154,7 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
self._fetch_dir(out, **kwargs)
|
||||
|
||||
base = out.path_info.parts
|
||||
for key, _ in out.obj: # noqa: B301
|
||||
for key, _, _ in out.obj: # noqa: B301
|
||||
trie[base + key] = None
|
||||
|
||||
def _walk(self, root, trie, topdown=True, **kwargs):
|
||||
@@ -166,16 +166,19 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
self._add_dir(trie, out, **kwargs)
|
||||
|
||||
root_len = len(root.parts)
|
||||
for key, out in trie.iteritems(prefix=root.parts): # noqa: B301
|
||||
if key == root.parts:
|
||||
continue
|
||||
try:
|
||||
for key, out in trie.iteritems(prefix=root.parts): # noqa: B301
|
||||
if key == root.parts:
|
||||
continue
|
||||
|
||||
name = key[root_len]
|
||||
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
|
||||
dirs.add(name)
|
||||
continue
|
||||
name = key[root_len]
|
||||
if len(key) > root_len + 1 or (out and out.is_dir_checksum):
|
||||
dirs.add(name)
|
||||
continue
|
||||
|
||||
files.append(name)
|
||||
files.append(name)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
assert topdown
|
||||
dirs = list(dirs)
|
||||
@@ -244,16 +247,16 @@ class DvcFileSystem(BaseFileSystem): # pylint:disable=abstract-method
|
||||
meta = self.metadata(path_info)
|
||||
ret = {"type": "directory" if meta.isdir else "file"}
|
||||
if meta.is_output and len(meta.outs) == 1 and meta.outs[0].hash_info:
|
||||
hash_info = meta.outs[0].hash_info
|
||||
ret["size"] = hash_info.size
|
||||
ret[hash_info.name] = hash_info.value
|
||||
out = meta.outs[0]
|
||||
ret["size"] = out.meta.size
|
||||
ret[out.hash_info.name] = out.hash_info.value
|
||||
elif meta.part_of_output:
|
||||
(out,) = meta.outs
|
||||
key = path_info.relative_to(out.path_info).parts
|
||||
obj = out.obj.trie.get(key)
|
||||
if obj:
|
||||
ret["size"] = obj.size
|
||||
ret[obj.hash_info.name] = obj.hash_info.value
|
||||
(obj_meta, oid) = out.obj.trie.get(key) or (None, None)
|
||||
if oid:
|
||||
ret["size"] = obj_meta.size if obj_meta else 0
|
||||
ret[oid.name] = oid.value
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
+2
-32
@@ -5,7 +5,7 @@ import re
|
||||
import threading
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from funcy import cached_property, retry, wrap_prop
|
||||
from funcy import cached_property, wrap_prop
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import CloudURLInfo
|
||||
@@ -34,36 +34,6 @@ class GDriveAuthError(DvcException):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def _gdrive_retry(func):
|
||||
def should_retry(exc):
|
||||
from pydrive2.files import ApiRequestError
|
||||
|
||||
if not isinstance(exc, ApiRequestError):
|
||||
return False
|
||||
|
||||
error_code = exc.error.get("code", 0)
|
||||
result = False
|
||||
if 500 <= error_code < 600:
|
||||
result = True
|
||||
|
||||
if error_code == 403:
|
||||
result = exc.GetField("reason") in [
|
||||
"userRateLimitExceeded",
|
||||
"rateLimitExceeded",
|
||||
]
|
||||
if result:
|
||||
logger.debug(f"Retrying GDrive API call, error: {exc}.")
|
||||
|
||||
return result
|
||||
|
||||
# 16 tries, start at 0.5s, multiply by golden ratio, cap at 20s
|
||||
return retry(
|
||||
16,
|
||||
timeout=lambda a: min(0.5 * 1.618 ** a, 20),
|
||||
filter_errors=should_retry,
|
||||
)(func)
|
||||
|
||||
|
||||
class GDriveURLInfo(CloudURLInfo):
|
||||
def __init__(self, url):
|
||||
super().__init__(url)
|
||||
@@ -219,7 +189,7 @@ class GDriveFileSystem(FSSpecWrapper): # pylint:disable=abstract-method
|
||||
temporary_save_path = self._gdrive_service_credentials_path
|
||||
|
||||
if is_credentials_temp:
|
||||
with open(temporary_save_path, "w") as cred_file:
|
||||
with open(temporary_save_path, "w", encoding="utf-8") as cred_file:
|
||||
cred_file.write(
|
||||
os.getenv(GDriveFileSystem.GDRIVE_CREDENTIALS_DATA)
|
||||
)
|
||||
|
||||
+3
-16
@@ -7,13 +7,8 @@ HASH_DIR_SUFFIX = ".dir"
|
||||
|
||||
@dataclass
|
||||
class HashInfo:
|
||||
PARAM_SIZE = "size"
|
||||
PARAM_NFILES = "nfiles"
|
||||
|
||||
name: Optional[str]
|
||||
value: Optional[str]
|
||||
size: Optional[int] = field(default=None, compare=False)
|
||||
nfiles: Optional[int] = field(default=None, compare=False)
|
||||
obj_name: Optional[str] = field(default=None, compare=False)
|
||||
|
||||
def __bool__(self):
|
||||
@@ -27,15 +22,11 @@ class HashInfo:
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
_d = d.copy() if d else {}
|
||||
size = _d.pop(cls.PARAM_SIZE, None)
|
||||
nfiles = _d.pop(cls.PARAM_NFILES, None)
|
||||
|
||||
if not _d:
|
||||
if not d:
|
||||
return cls(None, None)
|
||||
|
||||
((name, value),) = _d.items()
|
||||
return cls(name, value, size=size, nfiles=nfiles)
|
||||
((name, value),) = d.items()
|
||||
return cls(name, value)
|
||||
|
||||
def to_dict(self):
|
||||
ret = OrderedDict()
|
||||
@@ -43,10 +34,6 @@ class HashInfo:
|
||||
return ret
|
||||
|
||||
ret[self.name] = self.value
|
||||
if self.size is not None:
|
||||
ret[self.PARAM_SIZE] = self.size
|
||||
if self.nfiles is not None:
|
||||
ret[self.PARAM_NFILES] = self.nfiles
|
||||
return ret
|
||||
|
||||
@property
|
||||
|
||||
+1
-1
@@ -393,7 +393,7 @@ def init(path):
|
||||
if os.path.exists(dvcignore):
|
||||
return dvcignore
|
||||
|
||||
with open(dvcignore, "w") as fobj:
|
||||
with open(dvcignore, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(
|
||||
"# Add patterns of files dvc should ignore, which could improve\n"
|
||||
"# the performance. Learn more at\n"
|
||||
|
||||
+5
-7
@@ -6,6 +6,7 @@ import uuid
|
||||
|
||||
import psutil
|
||||
|
||||
from dvc import __version__
|
||||
from dvc.exceptions import DvcException, NotDvcRepoError
|
||||
from dvc.fs import FS_MAP, get_fs_cls, get_fs_config
|
||||
from dvc.repo import Repo
|
||||
@@ -13,12 +14,6 @@ from dvc.scm.base import SCMError
|
||||
from dvc.system import System
|
||||
from dvc.utils import error_link
|
||||
from dvc.utils.pkg import PKG
|
||||
from dvc.version import __version__
|
||||
|
||||
if PKG is None:
|
||||
package = ""
|
||||
else:
|
||||
package = f"({PKG})"
|
||||
|
||||
try:
|
||||
import importlib.metadata as importlib_metadata
|
||||
@@ -26,6 +21,9 @@ except ImportError: # < 3.8
|
||||
import importlib_metadata # type: ignore[no-redef]
|
||||
|
||||
|
||||
package = "" if PKG is None else f"({PKG})"
|
||||
|
||||
|
||||
def get_dvc_info():
|
||||
info = [
|
||||
f"DVC version: {__version__} {package}",
|
||||
@@ -93,7 +91,7 @@ def _get_linktype_support_info(repo):
|
||||
|
||||
fname = "." + str(uuid.uuid4())
|
||||
src = os.path.join(repo.odb.local.cache_dir, fname)
|
||||
open(src, "w").close()
|
||||
open(src, "w", encoding="utf-8").close()
|
||||
dst = os.path.join(repo.root_dir, fname)
|
||||
|
||||
cache = []
|
||||
|
||||
+2
-13
@@ -176,25 +176,14 @@ class HardlinkLock(flufl.lock.Lock, LockBase):
|
||||
except flufl.lock.TimeOutError:
|
||||
raise LockError(FAILED_TO_LOCK_MESSAGE)
|
||||
|
||||
def _set_claimfile(self, pid=None):
|
||||
super()._set_claimfile(pid)
|
||||
def _set_claimfile(self):
|
||||
super()._set_claimfile()
|
||||
|
||||
if self._tmp_dir is not None:
|
||||
# Under Windows file path length is limited so we hash it
|
||||
filename = hashlib.md5(self._claimfile.encode()).hexdigest()
|
||||
self._claimfile = os.path.join(self._tmp_dir, filename + ".lock")
|
||||
|
||||
# Fix for __del__ bug in flufl.lock [1] which is causing errors on
|
||||
# Python shutdown [2].
|
||||
# [1] https://gitlab.com/warsaw/flufl.lock/issues/7
|
||||
# [2] https://github.com/iterative/dvc/issues/2573
|
||||
def __del__(self):
|
||||
try:
|
||||
if self._owned:
|
||||
self.finalize()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def make_lock(lockfile, tmp_dir=None, friendly=False, hardlink_lock=False):
|
||||
cls = HardlinkLock if hardlink_lock else Lock
|
||||
|
||||
+12
-1
@@ -2,8 +2,10 @@ import logging
|
||||
import os
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Optional,
|
||||
Tuple,
|
||||
@@ -181,7 +183,7 @@ class MachineManager:
|
||||
"""Create and start the specified machine instance."""
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
if "startup_script" in config:
|
||||
with open(config["startup_script"]) as fobj:
|
||||
with open(config["startup_script"], encoding="utf-8") as fobj:
|
||||
startup_script = fobj.read()
|
||||
else:
|
||||
startup_script = DEFAULT_STARTUP_SCRIPT
|
||||
@@ -200,3 +202,12 @@ class MachineManager:
|
||||
def run_shell(self, name: Optional[str]):
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.run_shell(**config)
|
||||
|
||||
def status(self, name: str) -> Iterator[Dict[Any, Any]]:
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.instances(**config)
|
||||
|
||||
def rename(self, name: str, new: str):
|
||||
"""move configuration to a new location if the machine is running."""
|
||||
config, backend = self.get_config_and_backend(name)
|
||||
return backend.rename(new=new, **config)
|
||||
|
||||
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
||||
|
||||
class BaseMachineBackend(ABC):
|
||||
def __init__(self, tmp_dir: "StrPath", **kwargs):
|
||||
raise NotImplementedError
|
||||
self.tmp_dir = tmp_dir
|
||||
|
||||
@abstractmethod
|
||||
def create(self, name: Optional[str] = None, **config):
|
||||
@@ -48,3 +48,7 @@ class BaseMachineBackend(ABC):
|
||||
) -> Iterator["SSHFileSystem"]:
|
||||
"""Return an sshfs instance for the default directory on the
|
||||
specified machine."""
|
||||
|
||||
@abstractmethod
|
||||
def rename(self, name: str, new: str, **config):
|
||||
"""Rename a machine instance."""
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from functools import partialmethod
|
||||
from typing import TYPE_CHECKING, Iterator, Optional
|
||||
|
||||
from tpi import TerraformProviderIterative, terraform
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.fs.ssh import SSHFileSystem
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
from .base import BaseMachineBackend
|
||||
|
||||
@@ -12,9 +14,42 @@ if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
|
||||
class TerraformBackend(BaseMachineBackend):
|
||||
def __init__(self, tmp_dir: "StrPath", **kwargs):
|
||||
super().__init__(tmp_dir)
|
||||
super().__init__(tmp_dir, **kwargs)
|
||||
makedirs(tmp_dir, exist_ok=True)
|
||||
|
||||
@contextmanager
|
||||
def make_tpi(self, name: str):
|
||||
from tpi import TPIError
|
||||
from tpi.terraform import TerraformBackend as TPIBackend
|
||||
|
||||
try:
|
||||
working_dir = os.path.join(self.tmp_dir, name)
|
||||
makedirs(working_dir, exist_ok=True)
|
||||
yield TPIBackend(working_dir=working_dir)
|
||||
except TPIError as exc:
|
||||
raise DvcException("TPI operation failed") from exc
|
||||
|
||||
def _tpi_func(self, fname, name: Optional[str] = None, **config):
|
||||
from tpi import TPIError
|
||||
|
||||
assert name
|
||||
with self.make_tpi(name) as tpi:
|
||||
func = getattr(tpi, fname)
|
||||
try:
|
||||
return func(name=name, **config)
|
||||
except TPIError as exc:
|
||||
raise DvcException(f"TPI {fname} failed") from exc
|
||||
|
||||
create = partialmethod(_tpi_func, "create") # type: ignore[assignment]
|
||||
destroy = partialmethod(_tpi_func, "destroy") # type: ignore[assignment]
|
||||
instances = partialmethod(
|
||||
_tpi_func, "instances"
|
||||
) # type: ignore[assignment]
|
||||
run_shell = partialmethod(
|
||||
_tpi_func, "run_shell"
|
||||
) # type: ignore[assignment]
|
||||
|
||||
def get_executor(
|
||||
self, name: Optional[str] = None, **config
|
||||
@@ -25,7 +60,11 @@ class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
|
||||
def get_sshfs( # pylint: disable=unused-argument
|
||||
self, name: Optional[str] = None, **config
|
||||
) -> Iterator["SSHFileSystem"]:
|
||||
resource = self._default_resource(name)
|
||||
from tpi import TerraformProviderIterative
|
||||
|
||||
assert name
|
||||
with self.make_tpi(name) as tpi:
|
||||
resource = tpi.default_resource(name)
|
||||
with TerraformProviderIterative.pemfile(resource) as pem:
|
||||
fs = SSHFileSystem(
|
||||
host=resource["instance_ip"],
|
||||
@@ -33,3 +72,22 @@ class TerraformBackend(terraform.TerraformBackend, BaseMachineBackend):
|
||||
keyfile=pem,
|
||||
)
|
||||
yield fs
|
||||
|
||||
def rename(self, name: str, new: str, **config):
|
||||
"""rename a dvc machine instance to a new name"""
|
||||
import shutil
|
||||
|
||||
mtype = "iterative_machine"
|
||||
|
||||
new_dir = os.path.join(self.tmp_dir, new)
|
||||
old_dir = os.path.join(self.tmp_dir, name)
|
||||
if os.path.exists(new_dir):
|
||||
raise DvcException(f"rename failed: path {new_dir} already exists")
|
||||
|
||||
if not os.path.exists(old_dir):
|
||||
return
|
||||
|
||||
with self.make_tpi(name) as tpi:
|
||||
tpi.state_mv(f"{mtype}.{name}", f"{mtype}.{new}", **config)
|
||||
|
||||
shutil.move(old_dir, new_dir)
|
||||
|
||||
@@ -14,8 +14,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def check(odb: "ObjectDB", obj: "HashFile", **kwargs):
|
||||
if isinstance(obj, Tree):
|
||||
for _, entry in obj:
|
||||
odb.check(entry.hash_info, **kwargs)
|
||||
for _, _, oid in obj:
|
||||
odb.check(oid, **kwargs)
|
||||
|
||||
odb.check(obj.hash_info, **kwargs)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from dvc.objects.db.slow_link_detection import ( # type: ignore[attr-defined]
|
||||
from dvc.objects.diff import ROOT
|
||||
from dvc.objects.diff import diff as odiff
|
||||
from dvc.objects.stage import stage
|
||||
from dvc.objects.tree import Tree
|
||||
from dvc.types import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -131,8 +130,8 @@ def _checkout_file(
|
||||
):
|
||||
"""The file is changed we need to checkout a new copy"""
|
||||
modified = False
|
||||
cache_info = cache.hash_to_path_info(change.new.obj.hash_info.value)
|
||||
if change.old.obj:
|
||||
cache_info = cache.hash_to_path_info(change.new.oid.value)
|
||||
if change.old.oid:
|
||||
if relink:
|
||||
if fs.iscopy(path_info) and _cache_is_copy(cache, path_info):
|
||||
cache.unprotect(path_info)
|
||||
@@ -148,7 +147,7 @@ def _checkout_file(
|
||||
modified = True
|
||||
|
||||
if state:
|
||||
state.save(path_info, fs, change.new.obj.hash_info)
|
||||
state.save(path_info, fs, change.new.oid)
|
||||
|
||||
if progress_callback:
|
||||
progress_callback(str(path_info))
|
||||
@@ -166,7 +165,7 @@ def _diff(
|
||||
):
|
||||
old = None
|
||||
try:
|
||||
_, old = stage(
|
||||
_, _, old = stage(
|
||||
cache,
|
||||
path_info,
|
||||
fs,
|
||||
@@ -213,7 +212,7 @@ def _checkout(
|
||||
if change.new.key != ROOT
|
||||
else path_info
|
||||
)
|
||||
if isinstance(change.new.obj, Tree):
|
||||
if change.new.oid.isdir:
|
||||
fs.makedirs(entry_path)
|
||||
continue
|
||||
|
||||
|
||||
+20
-21
@@ -2,6 +2,8 @@ from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.hash_info import HashInfo
|
||||
|
||||
from .file import HashFile
|
||||
|
||||
ADD = "add"
|
||||
@@ -14,22 +16,19 @@ UNCHANGED = "unchanged"
|
||||
class TreeEntry:
|
||||
in_cache: bool
|
||||
key: Tuple[str]
|
||||
obj: Optional["HashFile"] = field(default=None)
|
||||
oid: Optional["HashInfo"] = field(default=None)
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.obj)
|
||||
return bool(self.oid)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, TreeEntry):
|
||||
return False
|
||||
|
||||
if self.key != other.key or bool(self.obj) != bool(other.obj):
|
||||
if self.key != other.key:
|
||||
return False
|
||||
|
||||
if not self.obj:
|
||||
return False
|
||||
|
||||
return self.obj.hash_info == other.obj.hash_info
|
||||
return self.oid == other.oid
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -83,39 +82,39 @@ def diff(
|
||||
if not obj:
|
||||
return []
|
||||
return [ROOT] + (
|
||||
[key for key, _ in obj] if isinstance(obj, Tree) else []
|
||||
[key for key, _, _ in obj] if isinstance(obj, Tree) else []
|
||||
)
|
||||
|
||||
old_keys = set(_get_keys(old))
|
||||
new_keys = set(_get_keys(new))
|
||||
|
||||
def _get_obj(obj, key):
|
||||
def _get_oid(obj, key):
|
||||
if not obj or key == ROOT:
|
||||
return obj
|
||||
return obj.hash_info if obj else None
|
||||
|
||||
return obj.get(key)
|
||||
entry_obj = obj.get(cache, key)
|
||||
return entry_obj.hash_info if entry_obj else None
|
||||
|
||||
def _in_cache(obj, cache):
|
||||
from . import check
|
||||
def _in_cache(oid, cache):
|
||||
from .errors import ObjectFormatError
|
||||
|
||||
if not obj:
|
||||
if not oid:
|
||||
return False
|
||||
|
||||
try:
|
||||
check(cache, obj)
|
||||
cache.check(oid)
|
||||
return True
|
||||
except (FileNotFoundError, ObjectFormatError):
|
||||
return False
|
||||
|
||||
ret = DiffResult()
|
||||
for key in old_keys | new_keys:
|
||||
old_obj = _get_obj(old, key)
|
||||
new_obj = _get_obj(new, key)
|
||||
old_oid = _get_oid(old, key)
|
||||
new_oid = _get_oid(new, key)
|
||||
|
||||
change = Change(
|
||||
old=TreeEntry(_in_cache(old_obj, cache), key, old_obj),
|
||||
new=TreeEntry(_in_cache(new_obj, cache), key, new_obj),
|
||||
old=TreeEntry(_in_cache(old_oid, cache), key, old_oid),
|
||||
new=TreeEntry(_in_cache(new_oid, cache), key, new_oid),
|
||||
)
|
||||
|
||||
if change.typ == ADD:
|
||||
@@ -126,8 +125,8 @@ def diff(
|
||||
ret.deleted.append(change)
|
||||
else:
|
||||
assert change.typ == UNCHANGED
|
||||
if not change.new.in_cache and not isinstance(
|
||||
change.new.obj, Tree
|
||||
if not change.new.in_cache and not (
|
||||
change.new.oid and change.new.oid.isdir
|
||||
):
|
||||
ret.modified.append(change)
|
||||
else:
|
||||
|
||||
+1
-5
@@ -28,10 +28,6 @@ class HashFile:
|
||||
self.hash_info = hash_info
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self.hash_info.size
|
||||
|
||||
def __len__(self):
|
||||
return 1
|
||||
|
||||
@@ -74,7 +70,7 @@ class HashFile:
|
||||
def _check_hash(self, odb):
|
||||
from .stage import get_file_hash
|
||||
|
||||
actual = get_file_hash(
|
||||
_, actual = get_file_hash(
|
||||
self.path_info, self.fs, self.hash_info.name, odb.state
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Meta:
|
||||
PARAM_SIZE = "size"
|
||||
PARAM_NFILES = "nfiles"
|
||||
|
||||
size: Optional[int] = field(default=None)
|
||||
nfiles: Optional[int] = field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
if not d:
|
||||
return cls()
|
||||
|
||||
size = d.pop(cls.PARAM_SIZE, None)
|
||||
nfiles = d.pop(cls.PARAM_NFILES, None)
|
||||
return cls(size=size, nfiles=nfiles)
|
||||
|
||||
def to_dict(self):
|
||||
ret = OrderedDict()
|
||||
|
||||
if self.size is not None:
|
||||
ret[self.PARAM_SIZE] = self.size
|
||||
|
||||
if self.nfiles is not None:
|
||||
ret[self.PARAM_NFILES] = self.nfiles
|
||||
|
||||
return ret
|
||||
+36
-41
@@ -14,6 +14,7 @@ from dvc.utils import file_md5
|
||||
|
||||
from .db.reference import ReferenceObjectDB
|
||||
from .file import HashFile
|
||||
from .meta import Meta
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.fs.base import BaseFileSystem
|
||||
@@ -38,7 +39,8 @@ def _upload_file(path_info, fs, odb, upload_odb):
|
||||
upload_odb.fs.upload(stream, tmp_info, desc=path_info.name, total=size)
|
||||
|
||||
odb.add(tmp_info, upload_odb.fs, stream.hash_info)
|
||||
return path_info, odb.get(stream.hash_info)
|
||||
meta = Meta(size=size)
|
||||
return path_info, meta, odb.get(stream.hash_info)
|
||||
|
||||
|
||||
def _get_file_hash(path_info, fs, name):
|
||||
@@ -54,31 +56,31 @@ def _get_file_hash(path_info, fs, name):
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return HashInfo(name, hash_value, size=info["size"])
|
||||
meta = Meta(size=info["size"])
|
||||
hash_info = HashInfo(name, hash_value)
|
||||
return meta, hash_info
|
||||
|
||||
|
||||
def get_file_hash(path_info, fs, name, state=None):
|
||||
if state:
|
||||
hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
meta, hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
path_info, fs
|
||||
)
|
||||
if hash_info:
|
||||
return hash_info
|
||||
return meta, hash_info
|
||||
|
||||
hash_info = _get_file_hash(path_info, fs, name)
|
||||
meta, hash_info = _get_file_hash(path_info, fs, name)
|
||||
|
||||
if state:
|
||||
assert ".dir" not in hash_info.value
|
||||
state.save(path_info, fs, hash_info)
|
||||
|
||||
return hash_info
|
||||
return meta, hash_info
|
||||
|
||||
|
||||
def _get_file_obj(
|
||||
path_info, fs, name, odb=None, upload_odb=None, dry_run=False
|
||||
):
|
||||
def _stage_file(path_info, fs, name, odb=None, upload_odb=None, dry_run=False):
|
||||
state = odb.state if odb else None
|
||||
hash_info = get_file_hash(path_info, fs, name, state=state)
|
||||
meta, hash_info = get_file_hash(path_info, fs, name, state=state)
|
||||
if upload_odb and not dry_run:
|
||||
assert odb and name == "md5"
|
||||
return _upload_file(path_info, fs, odb, upload_odb)
|
||||
@@ -88,7 +90,8 @@ def _get_file_obj(
|
||||
else:
|
||||
odb.add(path_info, fs, hash_info, move=False)
|
||||
obj = odb.get(hash_info)
|
||||
return path_info, obj
|
||||
|
||||
return path_info, meta, obj
|
||||
|
||||
|
||||
def _build_objects(
|
||||
@@ -111,7 +114,7 @@ def _build_objects(
|
||||
) as pbar:
|
||||
worker = pbar.wrap_fn(
|
||||
partial(
|
||||
_get_file_obj,
|
||||
_stage_file,
|
||||
fs=fs,
|
||||
name=name,
|
||||
**kwargs,
|
||||
@@ -130,8 +133,9 @@ def _iter_objects(path_info, fs, name, **kwargs):
|
||||
def _build_tree(path_info, fs, name, **kwargs):
|
||||
from .tree import Tree
|
||||
|
||||
tree_meta = Meta(size=0, nfiles=0)
|
||||
tree = Tree(None, None, None)
|
||||
for file_info, obj in _iter_objects(path_info, fs, name, **kwargs):
|
||||
for file_info, meta, obj in _iter_objects(path_info, fs, name, **kwargs):
|
||||
if DvcIgnore.DVCIGNORE_FILE == file_info.name:
|
||||
raise DvcIgnoreInCollectedDirError(file_info.parent)
|
||||
|
||||
@@ -143,11 +147,15 @@ def _build_tree(path_info, fs, name, **kwargs):
|
||||
#
|
||||
# Yes, this is a BUG, as long as we permit "/" in
|
||||
# filenames on Windows and "\" on Unix
|
||||
tree.add(file_info.relative_to(path_info).parts, obj)
|
||||
return tree
|
||||
tree.add(file_info.relative_to(path_info).parts, meta, obj.hash_info)
|
||||
|
||||
tree_meta.size += meta.size
|
||||
tree_meta.nfiles += 1
|
||||
|
||||
return tree_meta, tree
|
||||
|
||||
|
||||
def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
|
||||
def _stage_tree(path_info, fs, fs_info, name, odb=None, **kwargs):
|
||||
from .tree import Tree
|
||||
|
||||
value = fs_info.get(name)
|
||||
@@ -155,21 +163,15 @@ def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
|
||||
hash_info = HashInfo(name, value)
|
||||
try:
|
||||
tree = Tree.load(odb, hash_info)
|
||||
# NOTE: loaded entries are naive objects with hash_infos but no
|
||||
# path_info. For staging trees, obj.path_info should be relative
|
||||
# to the staging src `path_info` and src fs
|
||||
for key, entry in tree:
|
||||
entry.fs = fs
|
||||
entry.path_info = path_info.joinpath(*key)
|
||||
return tree
|
||||
return Meta(nfiles=len(tree)), tree
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
tree = _build_tree(path_info, fs, name, odb=odb, **kwargs)
|
||||
meta, tree = _build_tree(path_info, fs, name, odb=odb, **kwargs)
|
||||
state = odb.state if odb and odb.state else None
|
||||
hash_info = None
|
||||
if state:
|
||||
hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
_, hash_info = state.get( # pylint: disable=assignment-from-none
|
||||
path_info, fs
|
||||
)
|
||||
tree.digest(hash_info=hash_info)
|
||||
@@ -181,7 +183,7 @@ def _get_tree_obj(path_info, fs, fs_info, name, odb=None, **kwargs):
|
||||
tree.fs.remove(tree.path_info)
|
||||
tree.fs = raw.fs
|
||||
tree.path_info = raw.path_info
|
||||
return tree
|
||||
return meta, tree
|
||||
|
||||
|
||||
_url_cache: Dict[str, str] = {}
|
||||
@@ -223,7 +225,7 @@ def _load_from_state(odb, staging, path_info, fs, name):
|
||||
from .tree import Tree
|
||||
|
||||
state = odb.state
|
||||
hash_info = state.get(path_info, fs)
|
||||
meta, hash_info = state.get(path_info, fs)
|
||||
if hash_info:
|
||||
for odb_ in (odb, staging):
|
||||
if odb_.exists(hash_info):
|
||||
@@ -231,16 +233,9 @@ def _load_from_state(odb, staging, path_info, fs, name):
|
||||
obj = load(odb_, hash_info)
|
||||
check(odb_, obj, check_hash=False)
|
||||
if isinstance(obj, Tree):
|
||||
obj.hash_info.nfiles = len(obj)
|
||||
for key, entry in obj:
|
||||
entry.fs = fs
|
||||
entry.path_info = path_info.joinpath(*key)
|
||||
else:
|
||||
obj.fs = fs
|
||||
obj.path_info = path_info
|
||||
meta.nfiles = len(obj)
|
||||
assert obj.hash_info.name == name
|
||||
obj.hash_info.size = hash_info.size
|
||||
return odb_, obj
|
||||
return odb_, meta, obj
|
||||
except (ObjectFormatError, FileNotFoundError):
|
||||
pass
|
||||
raise FileNotFoundError
|
||||
@@ -255,7 +250,7 @@ def _stage_external_tree_info(odb, tree, name):
|
||||
|
||||
odb.add(tree.path_info, tree.fs, tree.hash_info)
|
||||
raw = odb.get(tree.hash_info)
|
||||
hash_info = get_file_hash(raw.path_info, raw.fs, name, state=odb.state)
|
||||
_, hash_info = get_file_hash(raw.path_info, raw.fs, name, state=odb.state)
|
||||
tree.path_info = raw.path_info
|
||||
tree.fs = raw.fs
|
||||
tree.hash_info.name = hash_info.name
|
||||
@@ -273,7 +268,7 @@ def stage(
|
||||
upload: bool = False,
|
||||
dry_run: bool = False,
|
||||
**kwargs,
|
||||
) -> Tuple["ObjectDB", "HashFile"]:
|
||||
) -> Tuple["ObjectDB", "Meta", "HashFile"]:
|
||||
"""Stage (prepare) objects from the given path for addition to an ODB.
|
||||
|
||||
Returns at tuple of (staging_odb, object) where addition to the ODB can
|
||||
@@ -298,7 +293,7 @@ def stage(
|
||||
pass
|
||||
|
||||
if details["type"] == "directory":
|
||||
obj = _get_tree_obj(
|
||||
meta, obj = _stage_tree(
|
||||
path_info,
|
||||
fs,
|
||||
details,
|
||||
@@ -312,7 +307,7 @@ def stage(
|
||||
if name != "md5":
|
||||
obj = _stage_external_tree_info(odb, obj, name)
|
||||
else:
|
||||
_, obj = _get_file_obj(
|
||||
_, meta, obj = _stage_file(
|
||||
path_info,
|
||||
fs,
|
||||
name,
|
||||
@@ -324,4 +319,4 @@ def stage(
|
||||
if odb and odb.state and obj.hash_info:
|
||||
odb.state.save(path_info, fs, obj.hash_info)
|
||||
|
||||
return staging, obj
|
||||
return staging, meta, obj
|
||||
|
||||
@@ -56,7 +56,7 @@ def _indexed_dir_hashes(odb, index, dir_objs, name, cache_odb):
|
||||
tree = Tree.load(cache_odb, HashInfo(name, dir_hash))
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
file_hashes = [entry.hash_info.value for _, entry in tree]
|
||||
file_hashes = [oid.value for _, _, oid in tree]
|
||||
if dir_hash not in index:
|
||||
logger.debug(
|
||||
"Indexing new .dir '%s' with '%s' nested files",
|
||||
@@ -102,9 +102,9 @@ def status(
|
||||
tree = None
|
||||
else:
|
||||
tree = Tree.load(cache_odb, hash_info)
|
||||
for _, entry in tree:
|
||||
assert entry.hash_info and entry.hash_info.value
|
||||
hash_infos[entry.hash_info.value] = entry.hash_info
|
||||
for _, _, oid in tree:
|
||||
assert oid and oid.value
|
||||
hash_infos[oid.value] = oid
|
||||
if index:
|
||||
dir_objs[hash_info.value] = tree
|
||||
hash_infos[hash_info.value] = hash_info
|
||||
|
||||
@@ -74,7 +74,7 @@ def _do_transfer(
|
||||
dir_obj = find_tree_by_obj_id([cache_odb, src], dir_hash)
|
||||
assert dir_obj
|
||||
|
||||
entry_ids = {entry.hash_info for _, entry in dir_obj}
|
||||
entry_ids = {oid for _, _, oid in dir_obj}
|
||||
bound_file_ids = all_file_ids & entry_ids
|
||||
all_file_ids -= entry_ids
|
||||
|
||||
@@ -117,7 +117,7 @@ def _do_transfer(
|
||||
# index successfully pushed dirs
|
||||
if dest_index:
|
||||
for dir_obj in succeeded_dir_objs:
|
||||
file_hashes = {entry.hash_info.value for _, entry in dir_obj}
|
||||
file_hashes = {oid.value for _, _, oid in dir_obj}
|
||||
logger.debug(
|
||||
"Indexing pushed dir '%s' with '%s' nested files",
|
||||
dir_obj.hash_info,
|
||||
|
||||
+26
-33
@@ -12,6 +12,8 @@ from .stage import get_file_hash
|
||||
if TYPE_CHECKING:
|
||||
from dvc.hash_info import HashInfo
|
||||
|
||||
from .meta import Meta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -20,7 +22,7 @@ class Tree(HashFile):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._dict: Dict[Tuple[str], "HashFile"] = {}
|
||||
self._dict: Dict[Tuple[str], Tuple["Meta", "HashInfo"]] = {}
|
||||
|
||||
@cached_property
|
||||
def trie(self):
|
||||
@@ -28,9 +30,9 @@ class Tree(HashFile):
|
||||
|
||||
return Trie(self._dict)
|
||||
|
||||
def add(self, key, obj):
|
||||
def add(self, key: Tuple[str], meta: "Meta", oid: "HashInfo"):
|
||||
self.__dict__.pop("trie", None)
|
||||
self._dict[key] = obj
|
||||
self._dict[key] = (meta, oid)
|
||||
|
||||
def digest(self, hash_info: Optional["HashInfo"] = None):
|
||||
from dvc.fs.memory import MemoryFileSystem
|
||||
@@ -46,20 +48,17 @@ class Tree(HashFile):
|
||||
if hash_info:
|
||||
self.hash_info = hash_info
|
||||
else:
|
||||
self.hash_info = get_file_hash(path_info, memfs, "md5")
|
||||
_, self.hash_info = get_file_hash(path_info, memfs, "md5")
|
||||
assert self.hash_info.value
|
||||
self.hash_info.value += ".dir"
|
||||
try:
|
||||
self.hash_info.size = sum(obj.size for _, obj in self)
|
||||
except TypeError:
|
||||
self.hash_info.size = None
|
||||
self.hash_info.nfiles = len(self)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dict)
|
||||
|
||||
def __iter__(self):
|
||||
yield from self._dict.items()
|
||||
yield from (
|
||||
(key, value[0], value[1]) for key, value in self._dict.items()
|
||||
)
|
||||
|
||||
def as_dict(self):
|
||||
return self._dict.copy()
|
||||
@@ -73,10 +72,10 @@ class Tree(HashFile):
|
||||
{
|
||||
# NOTE: not using hash_info.to_dict() because we don't want
|
||||
# size/nfiles fields at this point.
|
||||
obj.hash_info.name: obj.hash_info.value,
|
||||
oid.name: oid.value,
|
||||
self.PARAM_RELPATH: posixpath.sep.join(parts),
|
||||
}
|
||||
for parts, obj in self._dict.items() # noqa: B301
|
||||
for parts, _, oid in self # noqa: B301
|
||||
),
|
||||
key=itemgetter(self.PARAM_RELPATH),
|
||||
)
|
||||
@@ -94,8 +93,7 @@ class Tree(HashFile):
|
||||
relpath = entry.pop(cls.PARAM_RELPATH)
|
||||
parts = tuple(relpath.split(posixpath.sep))
|
||||
hash_info = HashInfo.from_dict(entry)
|
||||
obj = HashFile(None, None, hash_info)
|
||||
tree.add(parts, obj)
|
||||
tree.add(parts, None, hash_info)
|
||||
return tree
|
||||
|
||||
@classmethod
|
||||
@@ -118,8 +116,6 @@ class Tree(HashFile):
|
||||
tree = cls.from_list(raw)
|
||||
tree.path_info = obj.path_info
|
||||
tree.fs = obj.fs
|
||||
for _, entry_obj in tree:
|
||||
entry_obj.fs = obj.fs
|
||||
tree.hash_info = hash_info
|
||||
|
||||
return tree
|
||||
@@ -135,37 +131,37 @@ class Tree(HashFile):
|
||||
"""
|
||||
tree = Tree(self.path_info, self.fs, self.hash_info)
|
||||
try:
|
||||
for key, obj in self.trie.items(prefix):
|
||||
tree.add(key, obj)
|
||||
for key, (meta, oid) in self.trie.items(prefix):
|
||||
tree.add(key, meta, oid)
|
||||
except KeyError:
|
||||
pass
|
||||
return tree
|
||||
|
||||
def get(self, prefix: Tuple[str]) -> Optional[HashFile]:
|
||||
def get(self, odb, prefix: Tuple[str]) -> Optional[HashFile]:
|
||||
"""Return object at the specified prefix in this tree.
|
||||
|
||||
Returns None if no object exists at the specified prefix.
|
||||
"""
|
||||
obj = self._dict.get(prefix)
|
||||
if obj:
|
||||
return obj
|
||||
_, oid = self._dict.get(prefix) or (None, None)
|
||||
if oid:
|
||||
return odb.get(oid)
|
||||
|
||||
tree = Tree(None, None, None)
|
||||
depth = len(prefix)
|
||||
try:
|
||||
for key, obj in self.trie.items(prefix):
|
||||
tree.add(key[depth:], obj)
|
||||
for key, (meta, entry_oid) in self.trie.items(prefix):
|
||||
tree.add(key[depth:], meta, entry_oid)
|
||||
except KeyError:
|
||||
return None
|
||||
tree.digest()
|
||||
return tree
|
||||
|
||||
|
||||
def _get_dir_size(odb, tree):
|
||||
def du(odb, tree):
|
||||
try:
|
||||
return sum(
|
||||
odb.fs.getsize(odb.hash_to_path_info(obj.hash_info.value))
|
||||
for _, obj in tree
|
||||
odb.fs.getsize(odb.hash_to_path_info(oid.value))
|
||||
for _, _, oid in tree
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
@@ -226,11 +222,8 @@ def merge(odb, ancestor_info, our_info, their_info):
|
||||
merged_dict = _merge(ancestor.as_dict(), our.as_dict(), their.as_dict())
|
||||
|
||||
merged = Tree(None, None, None)
|
||||
for key, hi in merged_dict.items():
|
||||
merged.add(key, hi)
|
||||
for key, (meta, oid) in merged_dict.items():
|
||||
merged.add(key, meta, oid)
|
||||
merged.digest()
|
||||
|
||||
odb.add(merged.path_info, merged.fs, merged.hash_info)
|
||||
hash_info = merged.hash_info
|
||||
hash_info.size = _get_dir_size(odb, merged)
|
||||
return hash_info
|
||||
return merged
|
||||
|
||||
+37
-21
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from copy import copy
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set, Type
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -26,6 +25,7 @@ from .hash_info import HashInfo
|
||||
from .istextfile import istextfile
|
||||
from .objects import Tree
|
||||
from .objects.errors import ObjectFormatError
|
||||
from .objects.meta import Meta
|
||||
from .objects.stage import stage as ostage
|
||||
from .objects.transfer import transfer as otransfer
|
||||
from .scheme import Schemes
|
||||
@@ -316,6 +316,7 @@ class Output:
|
||||
# By resolved path, which contains actual location,
|
||||
# should be absolute and don't contain remote:// refs.
|
||||
self.stage = stage
|
||||
self.meta = Meta.from_dict(info)
|
||||
self.hash_info = HashInfo.from_dict(info)
|
||||
self.use_cache = False if self.IS_DEPENDENCY else cache
|
||||
self.metric = False if self.IS_DEPENDENCY else metric
|
||||
@@ -415,7 +416,7 @@ class Output:
|
||||
else:
|
||||
odb = self.repo.odb.local
|
||||
name = self.fs.PARAM_CHECKSUM
|
||||
_, obj = ostage(
|
||||
_, _, obj = ostage(
|
||||
odb,
|
||||
self.path_info,
|
||||
self.fs,
|
||||
@@ -537,7 +538,15 @@ class Output:
|
||||
self.verify_metric()
|
||||
|
||||
if not self.use_cache:
|
||||
self.hash_info = self.get_hash()
|
||||
_, self.meta, obj = ostage(
|
||||
self.repo.odb.local,
|
||||
self.path_info,
|
||||
self.fs,
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
dvcignore=self.dvcignore,
|
||||
dry_run=True,
|
||||
)
|
||||
self.hash_info = obj.hash_info
|
||||
if not self.IS_DEPENDENCY:
|
||||
logger.debug(
|
||||
"Output '%s' doesn't use cache. Skipping saving.", self
|
||||
@@ -550,7 +559,7 @@ class Output:
|
||||
logger.debug("Output '%s' didn't change. Skipping saving.", self)
|
||||
return
|
||||
|
||||
_, self.obj = ostage(
|
||||
_, self.meta, self.obj = ostage(
|
||||
self.odb,
|
||||
self.path_info,
|
||||
self.fs,
|
||||
@@ -579,7 +588,7 @@ class Output:
|
||||
if granular:
|
||||
obj = self._commit_granular_dir(filter_info)
|
||||
else:
|
||||
staging, obj = ostage(
|
||||
staging, _, obj = ostage(
|
||||
self.odb,
|
||||
filter_info or self.path_info,
|
||||
self.fs,
|
||||
@@ -606,7 +615,7 @@ class Output:
|
||||
|
||||
def _commit_granular_dir(self, filter_info):
|
||||
prefix = filter_info.relative_to(self.path_info).parts
|
||||
staging, save_obj = ostage(
|
||||
staging, _, save_obj = ostage(
|
||||
self.odb,
|
||||
self.path_info,
|
||||
self.fs,
|
||||
@@ -614,18 +623,18 @@ class Output:
|
||||
dvcignore=self.dvcignore,
|
||||
)
|
||||
save_obj = save_obj.filter(prefix)
|
||||
checkout_obj = save_obj.get(prefix)
|
||||
checkout_obj = save_obj.get(self.odb, prefix)
|
||||
otransfer(
|
||||
staging,
|
||||
self.odb,
|
||||
{save_obj.hash_info} | {entry.hash_info for _, entry in save_obj},
|
||||
{save_obj.hash_info} | {oid for _, _, oid in save_obj},
|
||||
shallow=True,
|
||||
move=True,
|
||||
)
|
||||
return checkout_obj
|
||||
|
||||
def dumpd(self):
|
||||
ret = copy(self.hash_info.to_dict())
|
||||
ret = {**self.hash_info.to_dict(), **self.meta.to_dict()}
|
||||
|
||||
if self.is_in_repo:
|
||||
path = self.path_info.relpath(self.stage.wdir).as_posix()
|
||||
@@ -709,7 +718,7 @@ class Output:
|
||||
|
||||
if filter_info and filter_info != self.path_info:
|
||||
prefix = filter_info.relative_to(self.path_info).parts
|
||||
obj = obj.get(prefix)
|
||||
obj = obj.get(self.odb, prefix)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -803,7 +812,7 @@ class Output:
|
||||
|
||||
upload = not (update and from_fs.isdir(from_info))
|
||||
jobs = jobs or min((from_fs.jobs, odb.fs.jobs))
|
||||
staging, obj = ostage(
|
||||
staging, self.meta, obj = ostage(
|
||||
odb,
|
||||
from_info,
|
||||
from_fs,
|
||||
@@ -832,7 +841,7 @@ class Output:
|
||||
return 1
|
||||
|
||||
if not filter_info or filter_info == self.path_info:
|
||||
return self.hash_info.nfiles or 0
|
||||
return self.meta.nfiles or 0
|
||||
|
||||
obj = self.get_obj(filter_info=filter_info)
|
||||
return len(obj) if obj else 0
|
||||
@@ -946,9 +955,9 @@ class Output:
|
||||
obj.hash_info.obj_name = name
|
||||
oids = {obj.hash_info}
|
||||
if isinstance(obj, Tree):
|
||||
for key, entry_obj in obj:
|
||||
entry_obj.hash_info.obj_name = self.fs.sep.join([name, *key])
|
||||
oids.add(entry_obj.hash_info)
|
||||
for key, _, oid in obj:
|
||||
oid.obj_name = self.fs.sep.join([name, *key])
|
||||
oids.add(oid)
|
||||
return oids
|
||||
|
||||
def get_used_external(
|
||||
@@ -981,8 +990,8 @@ class Output:
|
||||
|
||||
ignored = [
|
||||
self.fs.PARAM_CHECKSUM,
|
||||
HashInfo.PARAM_SIZE,
|
||||
HashInfo.PARAM_NFILES,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
]
|
||||
|
||||
for opt in ignored:
|
||||
@@ -1000,7 +1009,7 @@ class Output:
|
||||
)
|
||||
|
||||
def merge(self, ancestor, other):
|
||||
from dvc.objects.tree import merge
|
||||
from dvc.objects.tree import du, merge
|
||||
|
||||
assert other
|
||||
|
||||
@@ -1013,9 +1022,16 @@ class Output:
|
||||
self._check_can_merge(self)
|
||||
self._check_can_merge(other)
|
||||
|
||||
self.hash_info = merge(
|
||||
merged = merge(
|
||||
self.odb, ancestor_info, self.hash_info, other.hash_info
|
||||
)
|
||||
self.odb.add(merged.path_info, merged.fs, merged.hash_info)
|
||||
|
||||
self.hash_info = merged.hash_info
|
||||
self.meta = Meta(
|
||||
size=du(self.odb, merged),
|
||||
nfiles=len(merged),
|
||||
)
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
@@ -1040,8 +1056,8 @@ ARTIFACT_SCHEMA = {
|
||||
Output.PARAM_PLOT: bool,
|
||||
Output.PARAM_PERSIST: bool,
|
||||
Output.PARAM_CHECKPOINT: bool,
|
||||
HashInfo.PARAM_SIZE: int,
|
||||
HashInfo.PARAM_NFILES: int,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Output.PARAM_ISEXEC: bool,
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -89,7 +89,7 @@ def write(
|
||||
|
||||
page_html = None
|
||||
if template_path:
|
||||
with open(template_path) as fobj:
|
||||
with open(template_path, encoding="utf-8") as fobj:
|
||||
page_html = fobj.read()
|
||||
|
||||
document = HTML(page_html, refresh_seconds=refresh_seconds)
|
||||
@@ -102,6 +102,6 @@ def write(
|
||||
|
||||
index = Path(os.path.join(path, "index.html"))
|
||||
|
||||
with open(index, "w") as fd:
|
||||
with open(index, "w", encoding="utf-8") as fd:
|
||||
fd.write(document.embed())
|
||||
return index
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import os.path
|
||||
from typing import Dict, List
|
||||
|
||||
import dpath
|
||||
import dpath.util
|
||||
|
||||
|
||||
def get_files(data: Dict) -> List:
|
||||
|
||||
+1
-1
@@ -147,7 +147,7 @@ class VegaRenderer(Renderer):
|
||||
def get_vega(self) -> Optional[str]:
|
||||
props = self._squash_props()
|
||||
|
||||
template = self.templates.load(props.get("template") or "default")
|
||||
template = self.templates.load(props.get("template", None))
|
||||
|
||||
if not props.get("x") and template.has_anchor("x"):
|
||||
props["append_index"] = True
|
||||
|
||||
+11
-9
@@ -163,7 +163,6 @@ class Repo:
|
||||
from dvc.data_cloud import DataCloud
|
||||
from dvc.fs.local import LocalFileSystem
|
||||
from dvc.lock import LockNoop, make_lock
|
||||
from dvc.machine import MachineManager
|
||||
from dvc.objects.db import ODBManager
|
||||
from dvc.repo.live import Live
|
||||
from dvc.repo.metrics import Metrics
|
||||
@@ -224,14 +223,6 @@ class Repo:
|
||||
self.params = Params(self)
|
||||
self.live = Live(self)
|
||||
|
||||
if self.tmp_dir and (
|
||||
self.config["feature"].get("machine", False)
|
||||
or env2bool("DVC_TEST")
|
||||
):
|
||||
self.machine = MachineManager(self)
|
||||
else:
|
||||
self.machine = None
|
||||
|
||||
self.stage_collection_error_handler: Optional[
|
||||
Callable[[str, Exception], None]
|
||||
] = None
|
||||
@@ -298,6 +289,17 @@ class Repo:
|
||||
|
||||
return Experiments(self)
|
||||
|
||||
@cached_property
|
||||
def machine(self):
|
||||
from dvc.machine import MachineManager
|
||||
|
||||
if self.tmp_dir and (
|
||||
self.config["feature"].get("machine", False)
|
||||
or env2bool("DVC_TEST")
|
||||
):
|
||||
return MachineManager(self)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fs(self) -> "BaseFileSystem":
|
||||
return self._fs
|
||||
|
||||
+3
-3
@@ -137,7 +137,7 @@ def _output_paths(repo, targets):
|
||||
)
|
||||
|
||||
if on_working_fs:
|
||||
_, obj = ostage(
|
||||
_, _, obj = ostage(
|
||||
repo.odb.local,
|
||||
output.path_info,
|
||||
repo.odb.local.fs,
|
||||
@@ -164,13 +164,13 @@ def _output_paths(repo, targets):
|
||||
|
||||
|
||||
def _dir_output_paths(path_info, obj, targets=None):
|
||||
for key, entry_obj in obj:
|
||||
for key, _, oid in obj:
|
||||
fname = path_info.joinpath(*key)
|
||||
if targets is None or any(
|
||||
fname.isin_or_eq(target) for target in targets
|
||||
):
|
||||
# pylint: disable=no-member
|
||||
yield str(fname), entry_obj.hash_info.value
|
||||
yield str(fname), oid.value
|
||||
|
||||
|
||||
def _filter_missing(repo_fs, paths):
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Dict, Iterable, Mapping, Optional
|
||||
|
||||
from funcy import cached_property, first
|
||||
|
||||
from dvc.dependency.param import MissingParamsError
|
||||
from dvc.env import DVCLIVE_RESUME
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.path_info import PathInfo
|
||||
@@ -350,9 +351,19 @@ class Experiments:
|
||||
)
|
||||
self.scm.add(self.args_file)
|
||||
|
||||
def _format_new_params_msg(self, new_params, config_path):
|
||||
"""Format an error message for when new parameters are identified"""
|
||||
new_param_count = len(new_params)
|
||||
pluralise = "s are" if new_param_count > 1 else " is"
|
||||
param_list = ", ".join(new_params)
|
||||
return (
|
||||
f"{new_param_count} parameter{pluralise} missing "
|
||||
f"from '{config_path}': {param_list}"
|
||||
)
|
||||
|
||||
def _update_params(self, params: dict):
|
||||
"""Update experiment params files with the specified values."""
|
||||
from dvc.utils.collections import merge_params
|
||||
from dvc.utils.collections import NewParamsFound, merge_params
|
||||
from dvc.utils.serialize import MODIFIERS
|
||||
|
||||
logger.debug("Using experiment params '%s'", params)
|
||||
@@ -362,7 +373,11 @@ class Experiments:
|
||||
suffix = path.suffix.lower()
|
||||
modify_data = MODIFIERS[suffix]
|
||||
with modify_data(path, fs=self.repo.fs) as data:
|
||||
merge_params(data, params[params_fname])
|
||||
try:
|
||||
merge_params(data, params[params_fname], allow_new=False)
|
||||
except NewParamsFound as e:
|
||||
msg = self._format_new_params_msg(e.new_params, path)
|
||||
raise MissingParamsError(msg)
|
||||
|
||||
# Force params file changes to be staged in git
|
||||
# Otherwise in certain situations the changes to params file may be
|
||||
|
||||
@@ -490,7 +490,7 @@ class BaseExecutor(ABC):
|
||||
push_cache=push_cache,
|
||||
run_cache=run_cache,
|
||||
)
|
||||
except BaseException as exc:
|
||||
except BaseException as exc: # pylint: disable=broad-except
|
||||
logger.warning(
|
||||
"Something went wrong while auto pushing experiment "
|
||||
f"to the remote '{git_remote}': {exc}"
|
||||
|
||||
@@ -64,7 +64,7 @@ class TempDirExecutor(BaseLocalExecutor):
|
||||
def _config(self, cache_dir):
|
||||
local_config = os.path.join(self.dvc_dir, "config.local")
|
||||
logger.debug("Writing experiments local config '%s'", local_config)
|
||||
with open(local_config, "w") as fobj:
|
||||
with open(local_config, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(f"[cache]\n dir = {cache_dir}")
|
||||
|
||||
def cleanup(self):
|
||||
|
||||
+30
-26
@@ -3,7 +3,7 @@ import io
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Callable, Dict, List, Optional
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Generator, List, Optional
|
||||
|
||||
from funcy import cached_property, first, project
|
||||
|
||||
@@ -20,6 +20,7 @@ from dvc.utils.serialize import LOADERS
|
||||
if TYPE_CHECKING:
|
||||
from dvc.output import Output
|
||||
from dvc.repo import Repo
|
||||
from dvc.types import DvcPath
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -47,10 +48,10 @@ class Plots:
|
||||
recursive: bool = False,
|
||||
onerror: Optional[Callable] = None,
|
||||
props: Optional[Dict] = None,
|
||||
) -> Dict[str, Dict]:
|
||||
) -> Generator[Dict, None, None]:
|
||||
"""Collects all props and data for plots.
|
||||
|
||||
Returns a structure like:
|
||||
Generator yielding a structure like:
|
||||
{rev: {plots.csv: {
|
||||
props: {x: ..., "header": ..., ...},
|
||||
data: "unstructured data (as stored for given extension)",
|
||||
@@ -59,30 +60,20 @@ class Plots:
|
||||
from dvc.utils.collections import ensure_list
|
||||
|
||||
targets = ensure_list(targets)
|
||||
data: Dict[str, Dict] = {}
|
||||
for rev in self.repo.brancher(revs=revs):
|
||||
# .brancher() adds unwanted workspace
|
||||
if revs is not None and rev not in revs:
|
||||
continue
|
||||
rev = rev or "workspace"
|
||||
data[rev] = self._collect_from_revision(
|
||||
revision=rev,
|
||||
targets=targets,
|
||||
recursive=recursive,
|
||||
onerror=onerror,
|
||||
props=props,
|
||||
)
|
||||
|
||||
errored = errored_revisions(data)
|
||||
if errored:
|
||||
from dvc.ui import ui
|
||||
|
||||
ui.error_write(
|
||||
"DVC failed to load some plots for following revisions: "
|
||||
f"'{', '.join(errored)}'."
|
||||
)
|
||||
|
||||
return data
|
||||
yield {
|
||||
rev: self._collect_from_revision(
|
||||
revision=rev,
|
||||
targets=targets,
|
||||
recursive=recursive,
|
||||
onerror=onerror,
|
||||
props=props,
|
||||
)
|
||||
}
|
||||
|
||||
@error_handler
|
||||
def _collect_from_revision(
|
||||
@@ -135,9 +126,22 @@ class Plots:
|
||||
if onerror is None:
|
||||
onerror = onerror_collect
|
||||
|
||||
return self.collect(
|
||||
data: Dict[str, Dict] = {}
|
||||
for rev_data in self.collect(
|
||||
targets, revs, recursive, onerror=onerror, props=props
|
||||
)
|
||||
):
|
||||
data.update(rev_data)
|
||||
|
||||
errored = errored_revisions(data)
|
||||
if errored:
|
||||
from dvc.ui import ui
|
||||
|
||||
ui.error_write(
|
||||
"DVC failed to load some plots for following revisions: "
|
||||
f"'{', '.join(errored)}'."
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def diff(self, *args, **kwargs):
|
||||
from .diff import diff
|
||||
@@ -161,7 +165,7 @@ class Plots:
|
||||
props = props or {}
|
||||
template = props.get("template")
|
||||
if template:
|
||||
self.templates.get_template(template)
|
||||
self.templates.load(template)
|
||||
|
||||
(out,) = self.repo.find_outs_by_path(path)
|
||||
if not out.plot and unset is not None:
|
||||
@@ -201,7 +205,7 @@ def _collect_plots(
|
||||
targets: List[str] = None,
|
||||
rev: str = None,
|
||||
recursive: bool = False,
|
||||
) -> Dict[str, Dict]:
|
||||
) -> Dict["DvcPath", Dict]:
|
||||
from dvc.repo.collect import collect
|
||||
|
||||
plots, path_infos = collect(
|
||||
|
||||
+95
-489
@@ -1,15 +1,23 @@
|
||||
import json
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Iterable, Optional
|
||||
|
||||
try:
|
||||
import importlib_resources
|
||||
except ImportError:
|
||||
import importlib.resources as importlib_resources # type: ignore[no-redef]
|
||||
|
||||
from funcy import cached_property
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
|
||||
class TemplateNotFoundError(DvcException):
|
||||
def __init__(self, path):
|
||||
super().__init__(f"Template '{path}' not found.")
|
||||
def __init__(self, name):
|
||||
super().__init__(f"Template '{name}' not found.")
|
||||
|
||||
|
||||
class BadTemplateError(DvcException):
|
||||
@@ -29,25 +37,9 @@ class Template:
|
||||
EXTENSION = ".json"
|
||||
ANCHOR = "<DVC_METRIC_{}>"
|
||||
|
||||
DEFAULT_CONTENT: Optional[Dict[str, Any]] = None
|
||||
DEFAULT_NAME: Optional[str] = None
|
||||
|
||||
def __init__(self, content=None, name=None):
|
||||
if content:
|
||||
self.content = content
|
||||
else:
|
||||
self.content = (
|
||||
json.dumps(
|
||||
self.DEFAULT_CONTENT,
|
||||
indent=self.INDENT,
|
||||
separators=self.SEPARATORS,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
self.name = name or self.DEFAULT_NAME
|
||||
assert self.content and self.name
|
||||
self.filename = self.name + self.EXTENSION
|
||||
def __init__(self, content, name):
|
||||
self.content = content
|
||||
self.name = name
|
||||
|
||||
def render(self, data, props=None):
|
||||
props = props or {}
|
||||
@@ -55,7 +47,7 @@ class Template:
|
||||
if self._anchor_str("data") not in self.content:
|
||||
anchor = self.anchor("data")
|
||||
raise BadTemplateError(
|
||||
f"Template '{self.filename}' is not using '{anchor}' anchor"
|
||||
f"Template '{self.name}' is not using '{anchor}' anchor"
|
||||
)
|
||||
|
||||
if props.get("x"):
|
||||
@@ -106,466 +98,86 @@ class Template:
|
||||
raise NoFieldInDataError(field)
|
||||
|
||||
|
||||
class DefaultTemplate(Template):
|
||||
DEFAULT_NAME = "default"
|
||||
|
||||
DEFAULT_CONTENT = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"data": {"values": Template.anchor("data")},
|
||||
"title": Template.anchor("title"),
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"mark": {"type": "line"},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("x_label"),
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("y_label"),
|
||||
"scale": {"zero": False},
|
||||
},
|
||||
"color": {"field": "rev", "type": "nominal"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ConfusionTemplate(Template):
|
||||
DEFAULT_NAME = "confusion"
|
||||
DEFAULT_CONTENT = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"data": {"values": Template.anchor("data")},
|
||||
"title": Template.anchor("title"),
|
||||
"facet": {"field": "rev", "type": "nominal"},
|
||||
"spec": {
|
||||
"transform": [
|
||||
{
|
||||
"aggregate": [{"op": "count", "as": "xy_count"}],
|
||||
"groupby": [Template.anchor("y"), Template.anchor("x")],
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": ["rev", Template.anchor("y")],
|
||||
"key": Template.anchor("x"),
|
||||
"value": 0,
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": ["rev", Template.anchor("x")],
|
||||
"key": Template.anchor("y"),
|
||||
"value": 0,
|
||||
},
|
||||
{
|
||||
"joinaggregate": [
|
||||
{"op": "max", "field": "xy_count", "as": "max_count"}
|
||||
],
|
||||
"groupby": [],
|
||||
},
|
||||
{
|
||||
"calculate": "datum.xy_count / datum.max_count",
|
||||
"as": "percent_of_max",
|
||||
},
|
||||
],
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": Template.anchor("x_label"),
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": Template.anchor("y_label"),
|
||||
},
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": "rect",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"encoding": {
|
||||
"color": {
|
||||
"field": "xy_count",
|
||||
"type": "quantitative",
|
||||
"title": "",
|
||||
"scale": {"domainMin": 0, "nice": True},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"mark": "text",
|
||||
"encoding": {
|
||||
"text": {"field": "xy_count", "type": "quantitative"},
|
||||
"color": {
|
||||
"condition": {
|
||||
"test": "datum.percent_of_max > 0.5",
|
||||
"value": "white",
|
||||
},
|
||||
"value": "black",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class NormalizedConfusionTemplate(Template):
|
||||
DEFAULT_NAME = "confusion_normalized"
|
||||
DEFAULT_CONTENT = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"data": {"values": Template.anchor("data")},
|
||||
"title": Template.anchor("title"),
|
||||
"facet": {"field": "rev", "type": "nominal"},
|
||||
"spec": {
|
||||
"transform": [
|
||||
{
|
||||
"aggregate": [{"op": "count", "as": "xy_count"}],
|
||||
"groupby": [Template.anchor("y"), Template.anchor("x")],
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": ["rev", Template.anchor("y")],
|
||||
"key": Template.anchor("x"),
|
||||
"value": 0,
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": ["rev", Template.anchor("x")],
|
||||
"key": Template.anchor("y"),
|
||||
"value": 0,
|
||||
},
|
||||
{
|
||||
"joinaggregate": [
|
||||
{"op": "sum", "field": "xy_count", "as": "sum_y"}
|
||||
],
|
||||
"groupby": [Template.anchor("y")],
|
||||
},
|
||||
{
|
||||
"calculate": "datum.xy_count / datum.sum_y",
|
||||
"as": "percent_of_y",
|
||||
},
|
||||
],
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": Template.anchor("x_label"),
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": Template.anchor("y_label"),
|
||||
},
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": "rect",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"encoding": {
|
||||
"color": {
|
||||
"field": "percent_of_y",
|
||||
"type": "quantitative",
|
||||
"title": "",
|
||||
"scale": {"domain": [0, 1]},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"mark": "text",
|
||||
"encoding": {
|
||||
"text": {
|
||||
"field": "percent_of_y",
|
||||
"type": "quantitative",
|
||||
"format": ".2f",
|
||||
},
|
||||
"color": {
|
||||
"condition": {
|
||||
"test": "datum.percent_of_y > 0.5",
|
||||
"value": "white",
|
||||
},
|
||||
"value": "black",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ScatterTemplate(Template):
|
||||
DEFAULT_NAME = "scatter"
|
||||
|
||||
DEFAULT_CONTENT = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"data": {"values": Template.anchor("data")},
|
||||
"title": Template.anchor("title"),
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"layer": [
|
||||
{
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("x_label"),
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("y_label"),
|
||||
"scale": {"zero": False},
|
||||
},
|
||||
"color": {"field": "rev", "type": "nominal"},
|
||||
},
|
||||
"layer": [
|
||||
{"mark": "point"},
|
||||
{
|
||||
"selection": {
|
||||
"label": {
|
||||
"type": "single",
|
||||
"nearest": True,
|
||||
"on": "mouseover",
|
||||
"encodings": ["x"],
|
||||
"empty": "none",
|
||||
"clear": "mouseout",
|
||||
}
|
||||
},
|
||||
"mark": "point",
|
||||
"encoding": {
|
||||
"opacity": {
|
||||
"condition": {
|
||||
"selection": "label",
|
||||
"value": 1,
|
||||
},
|
||||
"value": 0,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"transform": [{"filter": {"selection": "label"}}],
|
||||
"layer": [
|
||||
{
|
||||
"encoding": {
|
||||
"text": {
|
||||
"type": "quantitative",
|
||||
"field": Template.anchor("y"),
|
||||
},
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "quantitative",
|
||||
},
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": {
|
||||
"type": "text",
|
||||
"align": "left",
|
||||
"dx": 5,
|
||||
"dy": -5,
|
||||
},
|
||||
"encoding": {
|
||||
"color": {
|
||||
"type": "nominal",
|
||||
"field": "rev",
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class SmoothLinearTemplate(Template):
|
||||
DEFAULT_NAME = "smooth"
|
||||
|
||||
DEFAULT_CONTENT = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"data": {"values": Template.anchor("data")},
|
||||
"title": Template.anchor("title"),
|
||||
"mark": {"type": "line"},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("x_label"),
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("y_label"),
|
||||
"scale": {"zero": False},
|
||||
},
|
||||
"color": {"field": "rev", "type": "nominal"},
|
||||
},
|
||||
"transform": [
|
||||
{
|
||||
"loess": Template.anchor("y"),
|
||||
"on": Template.anchor("x"),
|
||||
"groupby": ["rev"],
|
||||
"bandwidth": 0.3,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class LinearTemplate(Template):
|
||||
DEFAULT_NAME = "linear"
|
||||
|
||||
DEFAULT_CONTENT = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"data": {"values": Template.anchor("data")},
|
||||
"title": Template.anchor("title"),
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"layer": [
|
||||
{
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("x_label"),
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "quantitative",
|
||||
"title": Template.anchor("y_label"),
|
||||
"scale": {"zero": False},
|
||||
},
|
||||
"color": {"field": "rev", "type": "nominal"},
|
||||
},
|
||||
"layer": [
|
||||
{"mark": "line"},
|
||||
{
|
||||
"selection": {
|
||||
"label": {
|
||||
"type": "single",
|
||||
"nearest": True,
|
||||
"on": "mouseover",
|
||||
"encodings": ["x"],
|
||||
"empty": "none",
|
||||
"clear": "mouseout",
|
||||
}
|
||||
},
|
||||
"mark": "point",
|
||||
"encoding": {
|
||||
"opacity": {
|
||||
"condition": {
|
||||
"selection": "label",
|
||||
"value": 1,
|
||||
},
|
||||
"value": 0,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"transform": [{"filter": {"selection": "label"}}],
|
||||
"layer": [
|
||||
{
|
||||
"mark": {"type": "rule", "color": "gray"},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
"encoding": {
|
||||
"text": {
|
||||
"type": "quantitative",
|
||||
"field": Template.anchor("y"),
|
||||
},
|
||||
"x": {
|
||||
"field": Template.anchor("x"),
|
||||
"type": "quantitative",
|
||||
},
|
||||
"y": {
|
||||
"field": Template.anchor("y"),
|
||||
"type": "quantitative",
|
||||
},
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": {
|
||||
"type": "text",
|
||||
"align": "left",
|
||||
"dx": 5,
|
||||
"dy": -5,
|
||||
},
|
||||
"encoding": {
|
||||
"color": {
|
||||
"type": "nominal",
|
||||
"field": "rev",
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class PlotTemplates:
|
||||
TEMPLATES_DIR = "plots"
|
||||
TEMPLATES = [
|
||||
DefaultTemplate,
|
||||
LinearTemplate,
|
||||
ConfusionTemplate,
|
||||
NormalizedConfusionTemplate,
|
||||
ScatterTemplate,
|
||||
SmoothLinearTemplate,
|
||||
]
|
||||
PKG_TEMPLATES_DIR = "templates"
|
||||
|
||||
@cached_property
|
||||
def templates_dir(self):
|
||||
return os.path.join(self.dvc_dir, self.TEMPLATES_DIR)
|
||||
|
||||
def get_template(self, path):
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
@staticmethod
|
||||
def _find(templates, template_name):
|
||||
for template in templates:
|
||||
if (
|
||||
template_name == template
|
||||
or template_name + ".json" == template
|
||||
):
|
||||
return template
|
||||
return None
|
||||
|
||||
if self.dvc_dir and os.path.exists(self.dvc_dir):
|
||||
t_path = os.path.join(self.templates_dir, path)
|
||||
if os.path.exists(t_path):
|
||||
return t_path
|
||||
def _find_in_project(self, name: str) -> Optional["StrPath"]:
|
||||
if os.path.exists(name):
|
||||
return name
|
||||
|
||||
all_templates = [
|
||||
if os.path.exists(self.templates_dir):
|
||||
templates = [
|
||||
os.path.join(root, file)
|
||||
for root, _, files in os.walk(self.templates_dir)
|
||||
for file in files
|
||||
]
|
||||
matches = [
|
||||
template
|
||||
for template in all_templates
|
||||
if os.path.splitext(template)[0] == t_path
|
||||
]
|
||||
if matches:
|
||||
assert len(matches) == 1
|
||||
return matches[0]
|
||||
found = self._find(templates, name)
|
||||
if found:
|
||||
return os.path.join(self.templates_dir, found)
|
||||
return None
|
||||
|
||||
raise TemplateNotFoundError(path)
|
||||
@staticmethod
|
||||
def _get_templates() -> Iterable[str]:
|
||||
if (
|
||||
importlib_resources.files(__package__)
|
||||
.joinpath(PlotTemplates.PKG_TEMPLATES_DIR)
|
||||
.is_dir()
|
||||
):
|
||||
entries = (
|
||||
importlib_resources.files(__package__)
|
||||
.joinpath(PlotTemplates.PKG_TEMPLATES_DIR)
|
||||
.iterdir()
|
||||
)
|
||||
return [entry.name for entry in entries]
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _load_from_pkg(name):
|
||||
templates = PlotTemplates._get_templates()
|
||||
found = PlotTemplates._find(templates, name)
|
||||
if found:
|
||||
return (
|
||||
(
|
||||
importlib_resources.files(__package__)
|
||||
/ PlotTemplates.PKG_TEMPLATES_DIR
|
||||
/ found
|
||||
)
|
||||
.read_bytes()
|
||||
.decode("utf-8")
|
||||
)
|
||||
return None
|
||||
|
||||
def load(self, name: str = None) -> Template:
|
||||
|
||||
if name is not None:
|
||||
template_path = self._find_in_project(name)
|
||||
if template_path:
|
||||
with open(template_path, "r", encoding="utf-8") as fd:
|
||||
content = fd.read()
|
||||
return Template(content, name)
|
||||
else:
|
||||
name = "linear"
|
||||
|
||||
content = self._load_from_pkg(name)
|
||||
if content:
|
||||
return Template(content, name)
|
||||
|
||||
raise TemplateNotFoundError(name)
|
||||
|
||||
def __init__(self, dvc_dir):
|
||||
self.dvc_dir = dvc_dir
|
||||
@@ -574,24 +186,18 @@ class PlotTemplates:
|
||||
from dvc.utils.fs import makedirs
|
||||
|
||||
makedirs(self.templates_dir, exist_ok=True)
|
||||
for t in self.TEMPLATES:
|
||||
self._dump(t())
|
||||
|
||||
def _dump(self, template):
|
||||
path = os.path.join(self.templates_dir, template.filename)
|
||||
with open(path, "w") as fd:
|
||||
fd.write(template.content)
|
||||
|
||||
def load(self, name):
|
||||
try:
|
||||
path = self.get_template(name)
|
||||
|
||||
with open(path) as fd:
|
||||
content = fd.read()
|
||||
|
||||
return Template(content, name=name)
|
||||
except TemplateNotFoundError:
|
||||
for template in self.TEMPLATES:
|
||||
if template.DEFAULT_NAME == name:
|
||||
return template()
|
||||
raise
|
||||
templates = self._get_templates()
|
||||
for template in templates:
|
||||
content = (
|
||||
importlib_resources.files(__package__)
|
||||
.joinpath(PlotTemplates.PKG_TEMPLATES_DIR)
|
||||
.joinpath(template)
|
||||
.read_text()
|
||||
)
|
||||
with open(
|
||||
os.path.join(self.templates_dir, template),
|
||||
"w",
|
||||
encoding="utf-8",
|
||||
) as fd:
|
||||
fd.write(content)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {
|
||||
"values": "<DVC_METRIC_DATA>"
|
||||
},
|
||||
"title": "<DVC_METRIC_TITLE>",
|
||||
"facet": {
|
||||
"field": "rev",
|
||||
"type": "nominal"
|
||||
},
|
||||
"spec": {
|
||||
"transform": [
|
||||
{
|
||||
"aggregate": [
|
||||
{
|
||||
"op": "count",
|
||||
"as": "xy_count"
|
||||
}
|
||||
],
|
||||
"groupby": [
|
||||
"<DVC_METRIC_Y>",
|
||||
"<DVC_METRIC_X>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": [
|
||||
"rev",
|
||||
"<DVC_METRIC_Y>"
|
||||
],
|
||||
"key": "<DVC_METRIC_X>",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": [
|
||||
"rev",
|
||||
"<DVC_METRIC_X>"
|
||||
],
|
||||
"key": "<DVC_METRIC_Y>",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"joinaggregate": [
|
||||
{
|
||||
"op": "max",
|
||||
"field": "xy_count",
|
||||
"as": "max_count"
|
||||
}
|
||||
],
|
||||
"groupby": []
|
||||
},
|
||||
{
|
||||
"calculate": "datum.xy_count / datum.max_count",
|
||||
"as": "percent_of_max"
|
||||
}
|
||||
],
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": "<DVC_METRIC_X_LABEL>"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": "<DVC_METRIC_Y_LABEL>"
|
||||
}
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": "rect",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"encoding": {
|
||||
"color": {
|
||||
"field": "xy_count",
|
||||
"type": "quantitative",
|
||||
"title": "",
|
||||
"scale": {
|
||||
"domainMin": 0,
|
||||
"nice": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"mark": "text",
|
||||
"encoding": {
|
||||
"text": {
|
||||
"field": "xy_count",
|
||||
"type": "quantitative"
|
||||
},
|
||||
"color": {
|
||||
"condition": {
|
||||
"test": "datum.percent_of_max > 0.5",
|
||||
"value": "white"
|
||||
},
|
||||
"value": "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {
|
||||
"values": "<DVC_METRIC_DATA>"
|
||||
},
|
||||
"title": "<DVC_METRIC_TITLE>",
|
||||
"facet": {
|
||||
"field": "rev",
|
||||
"type": "nominal"
|
||||
},
|
||||
"spec": {
|
||||
"transform": [
|
||||
{
|
||||
"aggregate": [
|
||||
{
|
||||
"op": "count",
|
||||
"as": "xy_count"
|
||||
}
|
||||
],
|
||||
"groupby": [
|
||||
"<DVC_METRIC_Y>",
|
||||
"<DVC_METRIC_X>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": [
|
||||
"rev",
|
||||
"<DVC_METRIC_Y>"
|
||||
],
|
||||
"key": "<DVC_METRIC_X>",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"impute": "xy_count",
|
||||
"groupby": [
|
||||
"rev",
|
||||
"<DVC_METRIC_X>"
|
||||
],
|
||||
"key": "<DVC_METRIC_Y>",
|
||||
"value": 0
|
||||
},
|
||||
{
|
||||
"joinaggregate": [
|
||||
{
|
||||
"op": "sum",
|
||||
"field": "xy_count",
|
||||
"as": "sum_y"
|
||||
}
|
||||
],
|
||||
"groupby": [
|
||||
"<DVC_METRIC_Y>"
|
||||
]
|
||||
},
|
||||
{
|
||||
"calculate": "datum.xy_count / datum.sum_y",
|
||||
"as": "percent_of_y"
|
||||
}
|
||||
],
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": "<DVC_METRIC_X_LABEL>"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "nominal",
|
||||
"sort": "ascending",
|
||||
"title": "<DVC_METRIC_Y_LABEL>"
|
||||
}
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": "rect",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"encoding": {
|
||||
"color": {
|
||||
"field": "percent_of_y",
|
||||
"type": "quantitative",
|
||||
"title": "",
|
||||
"scale": {
|
||||
"domain": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"mark": "text",
|
||||
"encoding": {
|
||||
"text": {
|
||||
"field": "percent_of_y",
|
||||
"type": "quantitative",
|
||||
"format": ".2f"
|
||||
},
|
||||
"color": {
|
||||
"condition": {
|
||||
"test": "datum.percent_of_y > 0.5",
|
||||
"value": "white"
|
||||
},
|
||||
"value": "black"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {
|
||||
"values": "<DVC_METRIC_DATA>"
|
||||
},
|
||||
"title": "<DVC_METRIC_TITLE>",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"layer": [
|
||||
{
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_X_LABEL>"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_Y_LABEL>",
|
||||
"scale": {
|
||||
"zero": false
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"field": "rev",
|
||||
"type": "nominal"
|
||||
}
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": "line"
|
||||
},
|
||||
{
|
||||
"selection": {
|
||||
"label": {
|
||||
"type": "single",
|
||||
"nearest": true,
|
||||
"on": "mouseover",
|
||||
"encodings": [
|
||||
"x"
|
||||
],
|
||||
"empty": "none",
|
||||
"clear": "mouseout"
|
||||
}
|
||||
},
|
||||
"mark": "point",
|
||||
"encoding": {
|
||||
"opacity": {
|
||||
"condition": {
|
||||
"selection": "label",
|
||||
"value": 1
|
||||
},
|
||||
"value": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"filter": {
|
||||
"selection": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"layer": [
|
||||
{
|
||||
"mark": {
|
||||
"type": "rule",
|
||||
"color": "gray"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"encoding": {
|
||||
"text": {
|
||||
"type": "quantitative",
|
||||
"field": "<DVC_METRIC_Y>"
|
||||
},
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "quantitative"
|
||||
}
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": {
|
||||
"type": "text",
|
||||
"align": "left",
|
||||
"dx": 5,
|
||||
"dy": -5
|
||||
},
|
||||
"encoding": {
|
||||
"color": {
|
||||
"type": "nominal",
|
||||
"field": "rev"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {
|
||||
"values": "<DVC_METRIC_DATA>"
|
||||
},
|
||||
"title": "<DVC_METRIC_TITLE>",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"layer": [
|
||||
{
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_X_LABEL>"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_Y_LABEL>",
|
||||
"scale": {
|
||||
"zero": false
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"field": "rev",
|
||||
"type": "nominal"
|
||||
}
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": "point"
|
||||
},
|
||||
{
|
||||
"selection": {
|
||||
"label": {
|
||||
"type": "single",
|
||||
"nearest": true,
|
||||
"on": "mouseover",
|
||||
"encodings": [
|
||||
"x"
|
||||
],
|
||||
"empty": "none",
|
||||
"clear": "mouseout"
|
||||
}
|
||||
},
|
||||
"mark": "point",
|
||||
"encoding": {
|
||||
"opacity": {
|
||||
"condition": {
|
||||
"selection": "label",
|
||||
"value": 1
|
||||
},
|
||||
"value": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"transform": [
|
||||
{
|
||||
"filter": {
|
||||
"selection": "label"
|
||||
}
|
||||
}
|
||||
],
|
||||
"layer": [
|
||||
{
|
||||
"encoding": {
|
||||
"text": {
|
||||
"type": "quantitative",
|
||||
"field": "<DVC_METRIC_Y>"
|
||||
},
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "quantitative"
|
||||
}
|
||||
},
|
||||
"layer": [
|
||||
{
|
||||
"mark": {
|
||||
"type": "text",
|
||||
"align": "left",
|
||||
"dx": 5,
|
||||
"dy": -5
|
||||
},
|
||||
"encoding": {
|
||||
"color": {
|
||||
"type": "nominal",
|
||||
"field": "rev"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {
|
||||
"values": "<DVC_METRIC_DATA>"
|
||||
},
|
||||
"title": "<DVC_METRIC_TITLE>",
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"mark": {
|
||||
"type": "line"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_X_LABEL>"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_Y_LABEL>",
|
||||
"scale": {
|
||||
"zero": false
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"field": "rev",
|
||||
"type": "nominal"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {
|
||||
"values": "<DVC_METRIC_DATA>"
|
||||
},
|
||||
"title": "<DVC_METRIC_TITLE>",
|
||||
"mark": {
|
||||
"type": "line"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "<DVC_METRIC_X>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_X_LABEL>"
|
||||
},
|
||||
"y": {
|
||||
"field": "<DVC_METRIC_Y>",
|
||||
"type": "quantitative",
|
||||
"title": "<DVC_METRIC_Y_LABEL>",
|
||||
"scale": {
|
||||
"zero": false
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"field": "rev",
|
||||
"type": "nominal"
|
||||
}
|
||||
},
|
||||
"transform": [
|
||||
{
|
||||
"loess": "<DVC_METRIC_Y>",
|
||||
"on": "<DVC_METRIC_X>",
|
||||
"groupby": [
|
||||
"rev"
|
||||
],
|
||||
"bandwidth": 0.3
|
||||
}
|
||||
]
|
||||
}
|
||||
+2
-2
@@ -35,7 +35,7 @@ class RWLockFileFormatError(DvcException):
|
||||
def _edit_rwlock(lock_dir):
|
||||
path = os.path.join(lock_dir, "rwlock")
|
||||
try:
|
||||
with open(path) as fobj:
|
||||
with open(path, encoding="utf-8") as fobj:
|
||||
lock = SCHEMA(json.load(fobj))
|
||||
except FileNotFoundError:
|
||||
lock = SCHEMA({})
|
||||
@@ -46,7 +46,7 @@ def _edit_rwlock(lock_dir):
|
||||
lock["read"] = defaultdict(list, lock["read"])
|
||||
lock["write"] = defaultdict(dict, lock["write"])
|
||||
yield lock
|
||||
with open(path, "w+") as fobj:
|
||||
with open(path, "w+", encoding="utf-8") as fobj:
|
||||
json.dump(lock, fobj)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ from collections.abc import Mapping
|
||||
from voluptuous import Any, Optional, Required, Schema
|
||||
|
||||
from dvc import dependency, output
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.objects.meta import Meta
|
||||
from dvc.output import CHECKSUMS_SCHEMA, Output
|
||||
from dvc.parsing import DO_KWD, FOREACH_KWD, VARS_KWD
|
||||
from dvc.parsing.versions import SCHEMA_KWD, lockfile_version_schema
|
||||
@@ -26,8 +26,8 @@ SINGLE_STAGE_SCHEMA = {
|
||||
DATA_SCHEMA = {
|
||||
**CHECKSUMS_SCHEMA,
|
||||
Required("path"): str,
|
||||
HashInfo.PARAM_SIZE: int,
|
||||
HashInfo.PARAM_NFILES: int,
|
||||
Meta.PARAM_SIZE: int,
|
||||
Meta.PARAM_NFILES: int,
|
||||
Output.PARAM_ISEXEC: bool,
|
||||
}
|
||||
LOCK_FILE_STAGE_SCHEMA = {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""DVC re-implementation of fsspec's dedicated async event loop."""
|
||||
import asyncio
|
||||
import os
|
||||
import threading
|
||||
from typing import List, Optional
|
||||
|
||||
from fsspec.asyn import ( # noqa: F401, pylint:disable=unused-import
|
||||
_selector_policy,
|
||||
sync,
|
||||
sync_wrapper,
|
||||
)
|
||||
|
||||
# dedicated async IO thread
|
||||
iothread: List[Optional[threading.Thread]] = [None]
|
||||
# global DVC event loop
|
||||
default_loop: List[Optional[asyncio.AbstractEventLoop]] = [None]
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
def get_loop() -> asyncio.AbstractEventLoop:
|
||||
"""Create or return the global DVC event loop."""
|
||||
if default_loop[0] is None:
|
||||
with lock:
|
||||
if default_loop[0] is None:
|
||||
with _selector_policy():
|
||||
default_loop[0] = asyncio.new_event_loop()
|
||||
loop = default_loop[0]
|
||||
th = threading.Thread(
|
||||
target=loop.run_forever, # type: ignore[attr-defined]
|
||||
name="dvcIO",
|
||||
)
|
||||
th.daemon = True
|
||||
th.start()
|
||||
iothread[0] = th
|
||||
assert default_loop[0] is not None
|
||||
return default_loop[0]
|
||||
|
||||
|
||||
class BaseAsyncObject:
|
||||
def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None):
|
||||
self._loop: asyncio.AbstractEventLoop = loop or get_loop()
|
||||
self._pid = os.getpid()
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
# AsyncMixin is not fork-safe
|
||||
assert self._pid == os.getpid()
|
||||
return self._loop
|
||||
@@ -195,7 +195,7 @@ class Git(Base):
|
||||
if not os.path.exists(gitignore):
|
||||
return
|
||||
|
||||
with open(gitignore) as fobj:
|
||||
with open(gitignore, encoding="utf-8") as fobj:
|
||||
lines = fobj.readlines()
|
||||
|
||||
filtered = list(filter(lambda x: x.strip() != entry.strip(), lines))
|
||||
@@ -204,14 +204,14 @@ class Git(Base):
|
||||
os.unlink(gitignore)
|
||||
return
|
||||
|
||||
with open(gitignore, "w") as fobj:
|
||||
with open(gitignore, "w", encoding="utf-8") as fobj:
|
||||
fobj.writelines(filtered)
|
||||
|
||||
self.track_file(relpath(gitignore))
|
||||
|
||||
def _install_hook(self, name):
|
||||
hook = self._hook_path(name)
|
||||
with open(hook, "w+") as fobj:
|
||||
with open(hook, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write(f"#!/bin/sh\nexec dvc git-hook {name} $@\n")
|
||||
|
||||
os.chmod(hook, 0o777)
|
||||
@@ -265,6 +265,9 @@ class Git(Base):
|
||||
],
|
||||
}
|
||||
|
||||
if "repos" not in config:
|
||||
config["repos"] = []
|
||||
|
||||
if entry not in config["repos"]:
|
||||
config["repos"].append(entry)
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class BaseGitBackend(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None):
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
|
||||
"""Iterate over all refs in the specified remote Git repo.
|
||||
|
||||
If base is specified, only refs which begin with base will be yielded.
|
||||
@@ -197,6 +197,7 @@ class BaseGitBackend(ABC):
|
||||
dest: str,
|
||||
force: bool = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Push refspec to a remote Git repo.
|
||||
|
||||
@@ -222,6 +223,7 @@ class BaseGitBackend(ABC):
|
||||
refspecs: Iterable[str],
|
||||
force: Optional[bool] = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Fetch refspecs from a remote Git repo.
|
||||
|
||||
@@ -349,5 +351,5 @@ class BaseGitBackend(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def validate_git_remote(self, url: str):
|
||||
def validate_git_remote(self, url: str, **kwargs):
|
||||
"""Verify that url is a valid git URL or remote name."""
|
||||
|
||||
@@ -23,13 +23,13 @@ from dvc.progress import Tqdm
|
||||
from dvc.scm.base import GitAuthError, InvalidRemoteSCMRepo, SCMError
|
||||
from dvc.utils import relpath
|
||||
|
||||
from ..objects import GitObject
|
||||
from .base import BaseGitBackend
|
||||
from ...objects import GitObject
|
||||
from ..base import BaseGitBackend
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dvc.types import StrPath
|
||||
|
||||
from ..objects import GitCommit
|
||||
from ...objects import GitCommit
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -80,6 +80,13 @@ class DulwichObject(GitObject):
|
||||
class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
"""Dulwich Git backend."""
|
||||
|
||||
from dulwich import client
|
||||
|
||||
from .asyncssh_vendor import AsyncSSHVendor
|
||||
|
||||
# monkeypatch dulwich client's default SSH vendor to use asyncssh
|
||||
client.get_ssh_vendor = AsyncSSHVendor
|
||||
|
||||
# Dulwich progress will return messages equivalent to git CLI,
|
||||
# our pbars should just display the messages as formatted by dulwich
|
||||
BAR_FMT_NOTOTAL = "{desc}{bar:b}|{postfix[info]} [{elapsed}]"
|
||||
@@ -354,14 +361,14 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
else:
|
||||
yield os.fsdecode(key)
|
||||
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None):
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
|
||||
from dulwich.client import HTTPUnauthorized, get_transport_and_path
|
||||
from dulwich.errors import NotGitRepository
|
||||
from dulwich.porcelain import get_remote_repo
|
||||
|
||||
try:
|
||||
_remote, location = get_remote_repo(self.repo, url)
|
||||
client, path = get_transport_and_path(location)
|
||||
client, path = get_transport_and_path(location, **kwargs)
|
||||
except Exception as exc:
|
||||
raise InvalidRemoteSCMRepo(url) from exc
|
||||
|
||||
@@ -389,6 +396,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
dest: str,
|
||||
force: bool = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from dulwich.client import HTTPUnauthorized, get_transport_and_path
|
||||
from dulwich.errors import NotGitRepository, SendPackError
|
||||
@@ -402,7 +410,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
|
||||
try:
|
||||
_remote, location = get_remote_repo(self.repo, url)
|
||||
client, path = get_transport_and_path(location)
|
||||
client, path = get_transport_and_path(location, **kwargs)
|
||||
except Exception as exc:
|
||||
raise SCMError(
|
||||
f"'{url}' is not a valid Git remote or URL"
|
||||
@@ -476,6 +484,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
refspecs: Iterable[str],
|
||||
force: Optional[bool] = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
from dulwich.client import get_transport_and_path
|
||||
from dulwich.objectspec import parse_reftuples
|
||||
@@ -504,7 +513,7 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
|
||||
try:
|
||||
_remote, location = get_remote_repo(self.repo, url)
|
||||
client, path = get_transport_and_path(location)
|
||||
client, path = get_transport_and_path(location, **kwargs)
|
||||
except Exception as exc:
|
||||
raise SCMError(
|
||||
f"'{url}' is not a valid Git remote or URL"
|
||||
@@ -659,13 +668,13 @@ class DulwichBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
) -> Optional[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
def validate_git_remote(self, url: str):
|
||||
def validate_git_remote(self, url: str, **kwargs):
|
||||
from dulwich.client import LocalGitClient, get_transport_and_path
|
||||
from dulwich.porcelain import get_remote_repo
|
||||
|
||||
try:
|
||||
_, location = get_remote_repo(self.repo, url)
|
||||
client, path = get_transport_and_path(location)
|
||||
client, path = get_transport_and_path(location, **kwargs)
|
||||
except Exception as exc:
|
||||
raise InvalidRemoteSCMRepo(url) from exc
|
||||
if isinstance(client, LocalGitClient) and not os.path.exists(
|
||||
@@ -0,0 +1,92 @@
|
||||
"""asyncssh SSH vendor for Dulwich."""
|
||||
from typing import List, Optional
|
||||
|
||||
from dulwich.client import SSHVendor
|
||||
|
||||
from dvc.scm.asyn import BaseAsyncObject, sync_wrapper
|
||||
|
||||
|
||||
class _StderrWrapper:
|
||||
def __init__(self, stderr):
|
||||
self.stderr = stderr
|
||||
|
||||
async def _readlines(self):
|
||||
lines = []
|
||||
while True:
|
||||
line = await self.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
readlines = sync_wrapper(_readlines)
|
||||
|
||||
|
||||
class AsyncSSHWrapper(BaseAsyncObject):
|
||||
def __init__(self, conn, proc, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.conn = conn
|
||||
self.proc = proc
|
||||
self.stderr = _StderrWrapper(proc.stderr)
|
||||
|
||||
def can_read(self) -> bool:
|
||||
# pylint:disable=protected-access
|
||||
return self.proc.stdout._session._recv_buf_len > 0
|
||||
|
||||
async def _read(self, n: Optional[int] = None) -> bytes:
|
||||
if self.proc.stdout.at_eof():
|
||||
return b""
|
||||
|
||||
return await self.proc.stdout.read(n=n if n is not None else -1)
|
||||
|
||||
read = sync_wrapper(_read)
|
||||
|
||||
def write(self, data: bytes):
|
||||
self.proc.stdin.write(data)
|
||||
|
||||
def close(self):
|
||||
self.conn.close()
|
||||
|
||||
|
||||
class AsyncSSHVendor(BaseAsyncObject, SSHVendor):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def _run_command(
|
||||
self,
|
||||
host: str,
|
||||
command: List[str],
|
||||
username: Optional[str] = None,
|
||||
port: Optional[int] = None,
|
||||
password: Optional[str] = None,
|
||||
key_filename: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Connect to an SSH server.
|
||||
|
||||
Run a command remotely and return a file-like object for interaction
|
||||
with the remote command.
|
||||
|
||||
Args:
|
||||
host: Host name
|
||||
command: Command to run (as argv array)
|
||||
username: Optional ame of user to log in as
|
||||
port: Optional SSH port to use
|
||||
password: Optional ssh password for login or private key
|
||||
key_filename: Optional path to private keyfile
|
||||
"""
|
||||
import asyncssh
|
||||
|
||||
conn = await asyncssh.connect(
|
||||
host,
|
||||
port=port,
|
||||
username=username,
|
||||
password=password,
|
||||
client_keys=[key_filename] if key_filename else [],
|
||||
known_hosts=None,
|
||||
encoding=None,
|
||||
)
|
||||
proc = await conn.create_process(command, encoding=None)
|
||||
return AsyncSSHWrapper(conn, proc)
|
||||
|
||||
run_command = sync_wrapper(_run_command)
|
||||
@@ -438,7 +438,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
for ref in Reference.iter_items(self.repo, common_path=base):
|
||||
yield ref.path
|
||||
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None):
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_refs_containing(self, rev: str, pattern: Optional[str] = None):
|
||||
@@ -465,6 +465,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
dest: str,
|
||||
force: bool = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -474,6 +475,7 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
refspecs: Iterable[str],
|
||||
force: Optional[bool] = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -627,5 +629,5 @@ class GitPythonBackend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
raise SCMError("Merge failed") from exc
|
||||
return None
|
||||
|
||||
def validate_git_remote(self, url: str):
|
||||
def validate_git_remote(self, url: str, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -374,6 +374,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
dest: str,
|
||||
force: bool = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -383,6 +384,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
refspecs: Iterable[str],
|
||||
force: Optional[bool] = False,
|
||||
on_diverged: Optional[Callable[[str, str], bool]] = None,
|
||||
**kwargs,
|
||||
):
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -537,7 +539,7 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
index.add(entry.path)
|
||||
index.write()
|
||||
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None):
|
||||
def iter_remote_refs(self, url: str, base: Optional[str] = None, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def status(
|
||||
@@ -584,5 +586,5 @@ class Pygit2Backend(BaseGitBackend): # pylint:disable=abstract-method
|
||||
self.repo.index.write()
|
||||
return None
|
||||
|
||||
def validate_git_remote(self, url: str):
|
||||
def validate_git_remote(self, url: str, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
+2
-4
@@ -19,13 +19,11 @@ class RunCacheNotFoundError(DvcException):
|
||||
|
||||
|
||||
def _get_cache_hash(cache, key=False):
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.objects.meta import Meta
|
||||
|
||||
if key:
|
||||
cache["outs"] = [out["path"] for out in cache.get("outs", [])]
|
||||
return dict_sha256(
|
||||
cache, exclude=[HashInfo.PARAM_SIZE, HashInfo.PARAM_NFILES]
|
||||
)
|
||||
return dict_sha256(cache, exclude=[Meta.PARAM_SIZE, Meta.PARAM_NFILES])
|
||||
|
||||
|
||||
def _can_hash(stage):
|
||||
|
||||
@@ -7,6 +7,7 @@ from funcy import cached_property, get_in, lcat, once, project
|
||||
|
||||
from dvc import dependency, output
|
||||
from dvc.hash_info import HashInfo
|
||||
from dvc.objects.meta import Meta
|
||||
from dvc.parsing import FOREACH_KWD, JOIN, DataResolver, EntryNotFound
|
||||
from dvc.parsing.versions import LOCKFILE_VERSION
|
||||
from dvc.path_info import PathInfo
|
||||
@@ -69,6 +70,7 @@ class StageLoader(Mapping):
|
||||
info = info.copy()
|
||||
info.pop("path", None)
|
||||
item.isexec = info.pop("isexec", None)
|
||||
item.meta = Meta.from_dict(info)
|
||||
item.hash_info = HashInfo.from_dict(info)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -164,6 +164,7 @@ def to_single_stage_lockfile(stage: "Stage") -> dict:
|
||||
ret = [
|
||||
(item.PARAM_PATH, item.def_path),
|
||||
*item.hash_info.to_dict().items(),
|
||||
*item.meta.to_dict().items(),
|
||||
]
|
||||
|
||||
if item.isexec:
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Union
|
||||
from funcy import concat, first, lsplit, rpartial, without
|
||||
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
from dvc.objects.meta import Meta
|
||||
|
||||
from ..hash_info import HashInfo
|
||||
from .exceptions import (
|
||||
MissingDataSource,
|
||||
StageExternalOutputsError,
|
||||
@@ -197,8 +197,8 @@ def compute_md5(stage):
|
||||
Output.PARAM_PERSIST,
|
||||
Output.PARAM_CHECKPOINT,
|
||||
Output.PARAM_ISEXEC,
|
||||
HashInfo.PARAM_SIZE,
|
||||
HashInfo.PARAM_NFILES,
|
||||
Meta.PARAM_SIZE,
|
||||
Meta.PARAM_NFILES,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+7
-5
@@ -38,7 +38,7 @@ class StateNoop(StateBase):
|
||||
pass
|
||||
|
||||
def get(self, path_info, fs): # pylint: disable=unused-argument
|
||||
return None
|
||||
return None, None
|
||||
|
||||
def save_link(self, path_info, fs):
|
||||
pass
|
||||
@@ -103,22 +103,24 @@ class State(StateBase): # pylint: disable=too-many-instance-attributes
|
||||
HashInfo or None: hash for the specified path info or None if it
|
||||
doesn't exist in the state database.
|
||||
"""
|
||||
from .objects.meta import Meta
|
||||
|
||||
if not isinstance(fs, LocalFileSystem):
|
||||
return None
|
||||
return None, None
|
||||
|
||||
try:
|
||||
mtime, size = get_mtime_and_size(path_info, fs, self.dvcignore)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return None, None
|
||||
|
||||
inode = get_inode(path_info)
|
||||
|
||||
value = self.md5s.get(inode)
|
||||
|
||||
if not value or value[0] != mtime or value[1] != str(size):
|
||||
return None
|
||||
return None, None
|
||||
|
||||
return HashInfo("md5", value[2], size=size)
|
||||
return Meta(size=size), HashInfo("md5", value[2])
|
||||
|
||||
def save_link(self, path_info, fs):
|
||||
"""Adds the specified path to the list of links created by dvc. This
|
||||
|
||||
+2
-2
@@ -60,13 +60,13 @@ class System:
|
||||
|
||||
@staticmethod
|
||||
def _reflink_linux(src, dst):
|
||||
import fcntl
|
||||
import fcntl # pylint: disable=import-error
|
||||
|
||||
FICLONE = 0x40049409
|
||||
|
||||
try:
|
||||
ret = 255
|
||||
with open(src) as s, open(dst, "w+") as d:
|
||||
with open(src, "rb") as s, open(dst, "wb+") as d:
|
||||
ret = fcntl.ioctl(d.fileno(), FICLONE, s.fileno())
|
||||
finally:
|
||||
if ret != 0:
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ class Console:
|
||||
text.overflow = None
|
||||
return self.write(text, styled=True)
|
||||
|
||||
return self.write(json.dumps(data))
|
||||
return self.write(json.dumps(data, default=default))
|
||||
|
||||
def write(
|
||||
self,
|
||||
|
||||
+2
-2
@@ -68,7 +68,7 @@ class Updater:
|
||||
self.fetch()
|
||||
return
|
||||
|
||||
with open(self.updater_file) as fobj:
|
||||
with open(self.updater_file, encoding="utf-8") as fobj:
|
||||
import json
|
||||
|
||||
try:
|
||||
@@ -105,7 +105,7 @@ class Updater:
|
||||
logger.debug(msg.format(exc))
|
||||
return
|
||||
|
||||
with open(self.updater_file, "w+") as fobj:
|
||||
with open(self.updater_file, "w+", encoding="utf-8") as fobj:
|
||||
json.dump(info, fobj)
|
||||
|
||||
def _notify(self, latest: str, pkg: Optional[str] = PKG) -> None:
|
||||
|
||||
@@ -6,6 +6,16 @@ from typing import Callable, Dict, Iterable, List, TypeVar, Union
|
||||
|
||||
from pygtrie import StringTrie as _StringTrie
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class NewParamsFound(DvcException):
|
||||
"""Thrown if new params were found during merge_params"""
|
||||
|
||||
def __init__(self, new_params: List, *args):
|
||||
self.new_params = new_params
|
||||
super().__init__("New params found during merge", *args)
|
||||
|
||||
|
||||
class PathStringTrie(_StringTrie):
|
||||
"""Trie based on platform-dependent separator for pathname components."""
|
||||
@@ -80,11 +90,27 @@ def chunk_dict(d: Dict[_KT, _VT], size: int = 1) -> List[Dict[_KT, _VT]]:
|
||||
return [{key: d[key] for key in chunk} for chunk in chunks(size, d)]
|
||||
|
||||
|
||||
def merge_params(src: Dict, to_update: Dict) -> Dict:
|
||||
"""Recursively merges params with benedict's syntax support in-place."""
|
||||
def merge_params(src: Dict, to_update: Dict, allow_new: bool = True) -> Dict:
|
||||
"""
|
||||
Recursively merges params with benedict's syntax support in-place.
|
||||
|
||||
Args:
|
||||
src (dict): source dictionary of parameters
|
||||
to_update (dict): dictionary of parameters to merge into src
|
||||
allow_new (bool): if False, raises an error if new keys would be
|
||||
added to src
|
||||
"""
|
||||
from ._benedict import benedict
|
||||
|
||||
data = benedict(src)
|
||||
|
||||
if not allow_new:
|
||||
new_params = list(
|
||||
set(to_update.keys()) - set(data.keypaths(indexes=True))
|
||||
)
|
||||
if new_params:
|
||||
raise NewParamsFound(new_params)
|
||||
|
||||
data.merge(to_update, overwrite=True)
|
||||
return src
|
||||
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ try:
|
||||
# file is created during dvc build
|
||||
from .build import PKG # noqa, pylint:disable=unused-import
|
||||
except ImportError:
|
||||
PKG = None
|
||||
PKG = None # type: ignore[assignment]
|
||||
|
||||
+1
-3
@@ -47,6 +47,4 @@ class HashedStreamReader(io.IOBase):
|
||||
|
||||
@property
|
||||
def hash_info(self):
|
||||
return HashInfo(
|
||||
self.PARAM_CHECKSUM, self.md5.hexdigest(), size=self.total_read
|
||||
)
|
||||
return HashInfo(self.PARAM_CHECKSUM, self.md5.hexdigest())
|
||||
|
||||
+6
-74
@@ -1,74 +1,6 @@
|
||||
# Used in setup.py, so don't pull any additional dependencies
|
||||
#
|
||||
# Based on:
|
||||
# - https://github.com/python/mypy/blob/master/mypy/version.py
|
||||
# - https://github.com/python/mypy/blob/master/mypy/git.py
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
_BASE_VERSION = "2.8.0"
|
||||
|
||||
|
||||
def _generate_version(base_version):
|
||||
"""Generate a version with information about the Git repository."""
|
||||
pkg_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
if not _is_git_repo(pkg_dir) or not _have_git():
|
||||
return base_version
|
||||
|
||||
if _is_release(pkg_dir, base_version) and not _is_dirty(pkg_dir):
|
||||
return base_version
|
||||
|
||||
return "{base_version}+{short_sha}{dirty}".format(
|
||||
base_version=base_version,
|
||||
short_sha=_git_revision(pkg_dir).decode("utf-8")[0:6],
|
||||
dirty=".mod" if _is_dirty(pkg_dir) else "",
|
||||
)
|
||||
|
||||
|
||||
def _is_git_repo(dir_path):
|
||||
"""Is the given directory version-controlled with Git?"""
|
||||
return os.path.exists(os.path.join(dir_path, ".git"))
|
||||
|
||||
|
||||
def _have_git():
|
||||
"""Can we run the git executable?"""
|
||||
try:
|
||||
subprocess.check_output(["git", "--help"])
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _is_release(dir_path, base_version):
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["git", "describe", "--tags", "--exact-match"],
|
||||
cwd=dir_path,
|
||||
stderr=subprocess.STDOUT,
|
||||
).decode("utf-8")
|
||||
tag = output.strip()
|
||||
return tag == base_version
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def _git_revision(dir_path):
|
||||
"""Get SHA of the HEAD of a Git repository."""
|
||||
return subprocess.check_output(
|
||||
["git", "rev-parse", "HEAD"], cwd=dir_path
|
||||
).strip()
|
||||
|
||||
|
||||
def _is_dirty(dir_path):
|
||||
"""Check whether a git repository has uncommitted changes."""
|
||||
try:
|
||||
subprocess.check_call(["git", "diff", "--quiet"], cwd=dir_path)
|
||||
return False
|
||||
except subprocess.CalledProcessError:
|
||||
return True
|
||||
|
||||
|
||||
__version__ = _generate_version(_BASE_VERSION)
|
||||
# pylint: disable=unused-import
|
||||
try:
|
||||
from ._dvc_version import version as __version__
|
||||
from ._dvc_version import version_tuple # noqa: F401
|
||||
except ImportError:
|
||||
__version__ = "UNKNOWN"
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# noqa: D300,D400
|
||||
# Copyright (c) 2016, Aaron Christianson
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
Monkey patch setuptools to write faster console_scripts with this format:
|
||||
|
||||
import sys
|
||||
from mymodule import entry_function
|
||||
sys.exit(entry_function())
|
||||
|
||||
This is better.
|
||||
|
||||
(c) 2016, Aaron Christianson
|
||||
http://github.com/ninjaaron/fast-entry_points
|
||||
"""
|
||||
import re
|
||||
|
||||
from setuptools.command import easy_install
|
||||
|
||||
TEMPLATE = r"""
|
||||
# -*- coding: utf-8 -*-
|
||||
# EASY-INSTALL-ENTRY-SCRIPT: '{3}','{4}','{5}'
|
||||
__requires__ = '{3}'
|
||||
import re
|
||||
import sys
|
||||
|
||||
from {0} import {1}
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||
sys.exit({2}())
|
||||
""".lstrip()
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_args(cls, dist, header=None): # noqa: D205,D400
|
||||
"""
|
||||
Yield write_script() argument tuples for a distribution's
|
||||
console_scripts and gui_scripts entry points.
|
||||
"""
|
||||
if header is None:
|
||||
# pylint: disable=E1101
|
||||
header = cls.get_header()
|
||||
spec = str(dist.as_requirement())
|
||||
for type_ in "console", "gui":
|
||||
group = type_ + "_scripts"
|
||||
for name, ep in dist.get_entry_map(group).items():
|
||||
# ensure_safe_name
|
||||
if re.search(r"[\\/]", name):
|
||||
raise ValueError("Path separators not allowed in script names")
|
||||
script_text = TEMPLATE.format(
|
||||
ep.module_name,
|
||||
ep.attrs[0],
|
||||
".".join(ep.attrs),
|
||||
spec,
|
||||
group,
|
||||
name,
|
||||
)
|
||||
# pylint: disable=E1101,protected-access
|
||||
args = cls._get_script_args(type_, name, header, script_text)
|
||||
yield from args
|
||||
|
||||
|
||||
# pylint: disable=E1101
|
||||
easy_install.ScriptWriter.get_args = get_args
|
||||
|
||||
|
||||
def main():
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
dests = sys.argv[1:] or ["."]
|
||||
filename = re.sub(r"\.pyc$", ".py", __file__)
|
||||
|
||||
for dst in dests:
|
||||
shutil.copy(filename, dst)
|
||||
manifest_path = os.path.join(dst, "MANIFEST.in")
|
||||
setup_path = os.path.join(dst, "setup.py")
|
||||
|
||||
# Insert the include statement to MANIFEST.in if not present
|
||||
with open(manifest_path, "a+") as manifest:
|
||||
manifest.seek(0)
|
||||
manifest_content = manifest.read()
|
||||
if "include fastentrypoints.py" not in manifest_content:
|
||||
manifest.write(
|
||||
("\n" if manifest_content else "")
|
||||
+ "include fastentrypoints.py"
|
||||
)
|
||||
|
||||
# Insert the import statement to setup.py if not present
|
||||
with open(setup_path, "a+") as setup:
|
||||
setup.seek(0)
|
||||
setup_content = setup.read()
|
||||
if "import fastentrypoints" not in setup_content:
|
||||
setup.seek(0)
|
||||
setup.truncate()
|
||||
setup.write("import fastentrypoints\n" + setup_content)
|
||||
@@ -1,3 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=48", "wheel", "setuptools_scm[toml]>=6.3.1"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "dvc/_dvc_version.py"
|
||||
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
include = '\.pyi?$'
|
||||
@@ -15,3 +22,58 @@ exclude = '''
|
||||
| dist
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
known_first_party = ["dvc", "tests"]
|
||||
line_length = 79
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_level = "debug"
|
||||
addopts = "-ra"
|
||||
markers = [
|
||||
"needs_internet: Might need network access for the tests",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
# Error output
|
||||
show_column_numbers = true
|
||||
show_error_codes = true
|
||||
show_error_context = true
|
||||
show_traceback = true
|
||||
pretty = true
|
||||
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
|
||||
ignore_missing_imports = true
|
||||
check_untyped_defs = false
|
||||
# Warnings
|
||||
warn_no_return = true
|
||||
warn_redundant_casts = true
|
||||
warn_unreachable = true
|
||||
files = ["dvc"]
|
||||
|
||||
[tool.pylint.master]
|
||||
extension-pkg-whitelist = ["pygit2"]
|
||||
init-hook = "import sys; sys.path.append(str('tests'))"
|
||||
load-plugins = ["pylint_pytest", "pylint_plugin_disable"]
|
||||
|
||||
[tool.pylint.message_control]
|
||||
disable = [
|
||||
"format", "refactoring", "spelling", "design", "no-self-use",
|
||||
"invalid-name", "misplaced-comparison-constant", "duplicate-code", "fixme",
|
||||
"unused-wildcard-import", "cyclic-import", "wrong-import-order",
|
||||
"wrong-import-position", "ungrouped-imports", "multiple-imports",
|
||||
"logging-format-interpolation", "logging-fstring-interpolation",
|
||||
"missing-function-docstring", "missing-module-docstring",
|
||||
"missing-class-docstring", "raise-missing-from", "import-outside-toplevel",
|
||||
]
|
||||
enable = ["c-extension-no-member", "no-else-return"]
|
||||
|
||||
[tool.pylint.typecheck]
|
||||
generated-members = ["pytest.lazy_fixture", "logging.TRACE", "logger.trace", "sys.getwindowsversion", "argparse.Namespace"]
|
||||
ignored-classes = ["Dvcfile"]
|
||||
ignored-modules = ["azure"]
|
||||
signature-mutators = ["funcy.decorators.decorator"]
|
||||
|
||||
[tool.pylint.variables]
|
||||
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
|
||||
ignored-argument-names = "_.*|^ignored_|^unused_|args|kwargs"
|
||||
|
||||
@@ -10,5 +10,5 @@ fi
|
||||
|
||||
echo 'PKG = "pip"' >dvc/utils/build.py
|
||||
|
||||
python setup.py sdist
|
||||
python setup.py bdist_wheel
|
||||
python -m pip install -U build setuptools>=58.2
|
||||
python -m build --sdist --wheel --outdir dist/
|
||||
|
||||
@@ -9,5 +9,6 @@ fi
|
||||
|
||||
rm -rf dist/
|
||||
pip install twine
|
||||
python setup.py sdist bdist_wheel
|
||||
python -m pip install -U build setuptools>=58.2
|
||||
python -m build --sdist --wheel --outdir dist/
|
||||
twine upload dist/*
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = Git for data scientists - manage your code and data together
|
||||
name = dvc
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/rst
|
||||
long_description_content_type = text/x-rst
|
||||
license = Apache License 2.0
|
||||
license_file = LICENSE
|
||||
url = http://dvc.org
|
||||
@@ -19,14 +19,16 @@ keywords = data-science, data-version-control, machine-learning, git
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
|
||||
[options]
|
||||
python_requires = >=3.6
|
||||
setup_requires =
|
||||
setuptools>=48
|
||||
setuptools_scm[toml]>=6.3.1
|
||||
python_requires = >=3.7
|
||||
zip_safe = False
|
||||
packages = find:
|
||||
include_package_data = True
|
||||
@@ -37,9 +39,7 @@ install_requires =
|
||||
configobj>=5.0.6
|
||||
gitpython>3
|
||||
dulwich>=0.20.23
|
||||
pygit2>=1.5.0,<1.7; python_version < '3.7'
|
||||
pygit2>=1.5.0; python_version >= '3.7'
|
||||
setuptools>=34.0.0
|
||||
pygit2>=1.5.0
|
||||
nanotime>=0.5.2
|
||||
pyasn1>=0.4.1
|
||||
voluptuous>=0.11.7
|
||||
@@ -51,19 +51,17 @@ install_requires =
|
||||
ruamel.yaml>=0.17.11
|
||||
toml>=0.10.1
|
||||
funcy>=1.14
|
||||
pathspec>=0.6.0,<0.9.0
|
||||
pathspec>=0.9.0,<0.10.0
|
||||
shortuuid>=0.5.0
|
||||
tqdm>=4.45.0,<5
|
||||
packaging>=19.0
|
||||
zc.lockfile>=1.2.1
|
||||
flufl.lock>=3.2,<4
|
||||
win-unicode-console>=0.5; sys_platform == 'win32'
|
||||
flufl.lock>=5
|
||||
networkx>=2.5
|
||||
psutil>=5.8.0
|
||||
pydot>=1.2.4
|
||||
dataclasses>=0.7; python_version < '3.7'
|
||||
contextvars>=2.1; python_version < '3.7'
|
||||
importlib-metadata>=1.4; python_version < '3.8'
|
||||
importlib-resources>=5.2.2; python_version < '3.9'
|
||||
flatten_dict>=0.4.1,<1
|
||||
tabulate>=0.8.7
|
||||
pygtrie>=2.3.2
|
||||
@@ -74,7 +72,7 @@ install_requires =
|
||||
python-benedict>=0.24.2
|
||||
pyparsing==2.4.7
|
||||
typing_extensions>=3.7.4
|
||||
fsspec[http]>=2021.10.0
|
||||
fsspec[http]>=2021.10.1
|
||||
aiohttp-retry>=2.4.5
|
||||
diskcache>=5.2.1
|
||||
|
||||
@@ -101,19 +99,19 @@ azure =
|
||||
azure-identity>=1.4.0
|
||||
knack
|
||||
gdrive = pydrive2[fsspec]>=1.9.4
|
||||
gs = gcsfs==2021.10.0
|
||||
gs = gcsfs==2021.10.1
|
||||
hdfs =
|
||||
fsspec[arrow]; python_version < '3.10'
|
||||
oss = ossfs==2021.8.0
|
||||
s3 =
|
||||
s3fs==2021.10.0
|
||||
s3fs==2021.10.1
|
||||
aiobotocore[boto3]>1.0.1
|
||||
ssh = sshfs[bcrypt]>=2021.8.1
|
||||
ssh_gssapi = sshfs[gssapi]>=2021.8.1
|
||||
webdav = webdav4>=0.9.1
|
||||
webdav = webdav4>=0.9.3
|
||||
# not to break `dvc[webhdfs]`
|
||||
webhdfs =
|
||||
terraform = tpi[ssh]>=0.0.0
|
||||
terraform = tpi[ssh]>=2.1.0
|
||||
tests =
|
||||
%(terraform)s
|
||||
wheel==0.37.0
|
||||
@@ -129,14 +127,13 @@ tests =
|
||||
rangehttpserver==1.2.0
|
||||
mock-ssh-server==0.9.1
|
||||
wget==3.2
|
||||
filelock==3.3.0
|
||||
filelock==3.3.1
|
||||
wsgidav==3.1.1
|
||||
crc32c==2.2.post0
|
||||
xmltodict==0.12.0
|
||||
google-compute-engine==2.8.13
|
||||
google-cloud-storage==1.42.3
|
||||
dvclive==0.4.0
|
||||
pywin32>=225; sys_platform == 'win32' and python_version < '3.10'
|
||||
dvclive==0.4.1
|
||||
hdfs==2.6.0
|
||||
# required by collective.checkdocs
|
||||
Pygments==2.10.0
|
||||
@@ -144,7 +141,7 @@ tests =
|
||||
pydocstyle==6.1.1
|
||||
jaraco.windows==5.7.0
|
||||
# pylint requirements
|
||||
pylint==2.9.6
|
||||
pylint==2.11.1
|
||||
# we use this to suppress pytest-related false positives in our tests.
|
||||
pylint-pytest==1.0.3
|
||||
# we use this to suppress some messages in tests, eg: foo/bar naming,
|
||||
@@ -152,10 +149,11 @@ tests =
|
||||
pylint-plugin-utils==0.6
|
||||
# type-checking
|
||||
mypy==0.910
|
||||
types-requests==2.25.9
|
||||
types-paramiko==2.7.1
|
||||
types-tabulate==0.8.2
|
||||
types-toml==0.10.0
|
||||
types-requests==2.25.11
|
||||
types-tabulate==0.8.3
|
||||
types-toml==0.10.1
|
||||
# optional dependencies
|
||||
pywin32>=225; sys_platform == 'win32'
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
@@ -173,37 +171,8 @@ ignore=
|
||||
W503, # Line break occurred before a binary operator
|
||||
P1, # unindexed parameters in the str.format, see:
|
||||
# https://pypi.org/project/flake8-string-format/
|
||||
max_line_length=79
|
||||
max_line_length = 79
|
||||
max-complexity = 15
|
||||
select=B,C,E,F,W,T4,B902,T,P
|
||||
show_source=true
|
||||
count=true
|
||||
|
||||
[isort]
|
||||
profile=black
|
||||
known_first_party=dvc,tests
|
||||
line_length=79
|
||||
|
||||
[tool:pytest]
|
||||
log_level = debug
|
||||
addopts = -ra
|
||||
markers =
|
||||
needs_internet: Might need network access for the tests
|
||||
|
||||
[mypy]
|
||||
# Error output
|
||||
show_column_numbers = True
|
||||
show_error_codes = True
|
||||
show_error_context = True
|
||||
show_traceback = True
|
||||
pretty = True
|
||||
|
||||
# See https://mypy.readthedocs.io/en/latest/running_mypy.html#missing-imports.
|
||||
ignore_missing_imports = True
|
||||
check_untyped_defs = False
|
||||
|
||||
# Warnings
|
||||
warn_no_return=True
|
||||
warn_redundant_casts=True
|
||||
warn_unreachable = True
|
||||
files = dvc
|
||||
select = B,C,E,F,W,T4,B902,T,P
|
||||
show_source = true
|
||||
count = true
|
||||
|
||||
@@ -1,66 +1,3 @@
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools.command.build_py import build_py as _build_py
|
||||
|
||||
# Prevents pkg_resources import in entry point script,
|
||||
# see https://github.com/ninjaaron/fast-entry_points.
|
||||
# This saves about 200 ms on startup time for non-wheel installs.
|
||||
try:
|
||||
import fastentrypoints # noqa: F401, pylint: disable=unused-import
|
||||
except ImportError:
|
||||
pass # not able to import when installing through pre-commit
|
||||
|
||||
|
||||
# Read package meta-data from version.py
|
||||
# see https://packaging.python.org/guides/single-sourcing-package-version/
|
||||
try:
|
||||
pkg_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
version_path = os.path.join(pkg_dir, "dvc", "version.py")
|
||||
spec = importlib.util.spec_from_file_location("dvc.version", version_path)
|
||||
dvc_version = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(dvc_version)
|
||||
version = dvc_version.__version__ # noqa: F821
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
# Dependabot seem to stop working when we don't handle this except block.
|
||||
# Most likely, it's because of the restrictions in execution of the module.
|
||||
# We workaround it, and print the error message if it also happens on other
|
||||
# installations (though this message may likely be suppressed by build
|
||||
# tools, eg: pip only shows this message on `--verbose` mode).
|
||||
import sys
|
||||
|
||||
print("Could not load version info: ", exc, file=sys.stderr)
|
||||
version = "UNKNOWN"
|
||||
|
||||
|
||||
# To achieve consistency between the build version and the one provided
|
||||
# by your package during runtime, you need to **pin** the build version.
|
||||
#
|
||||
# This custom class will replace the version.py module with a **static**
|
||||
# `__version__` that your package can read at runtime, assuring consistency.
|
||||
#
|
||||
# References:
|
||||
# - https://docs.python.org/3.7/distutils/extending.html
|
||||
# - https://github.com/python/mypy
|
||||
class build_py(_build_py):
|
||||
def pin_version(self):
|
||||
ver_file = Path(self.build_lib) / "dvc" / "version.py"
|
||||
self.mkpath(os.fspath(ver_file.parent))
|
||||
ver_file.write_text(
|
||||
dedent(
|
||||
f"""\
|
||||
# AUTOGENERATED at build time by setup.py
|
||||
__version__ = "{version}"
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.execute(self.pin_version, ())
|
||||
_build_py.run(self)
|
||||
|
||||
|
||||
setup(version=version, cmdclass={"build_py": build_py})
|
||||
setup()
|
||||
|
||||
+7
-4
@@ -5,9 +5,12 @@ import sys
|
||||
if os.name == "nt":
|
||||
import subprocess
|
||||
|
||||
import win32file # pylint: disable=import-error
|
||||
|
||||
win32file._setmaxstdio(4096)
|
||||
try:
|
||||
import win32file # pylint: disable=import-error
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
win32file._setmaxstdio(4096)
|
||||
|
||||
# Workaround for two bugs:
|
||||
#
|
||||
@@ -34,7 +37,7 @@ if os.name == "nt":
|
||||
subprocess._cleanup = noop
|
||||
subprocess._active = None
|
||||
else:
|
||||
import resource
|
||||
import resource # pylint: disable=import-error
|
||||
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (4096, 4096))
|
||||
|
||||
|
||||
+11
-2
@@ -161,10 +161,19 @@ def pytest_configure(config):
|
||||
|
||||
@pytest.fixture()
|
||||
def custom_template(tmp_dir, dvc):
|
||||
import shutil
|
||||
try:
|
||||
import importlib_resources
|
||||
except ImportError:
|
||||
import importlib.resources as importlib_resources
|
||||
|
||||
content = (
|
||||
importlib_resources.files("dvc.repo.plots")
|
||||
/ "templates"
|
||||
/ "simple.json"
|
||||
).read_text()
|
||||
|
||||
template = tmp_dir / "custom_template.json"
|
||||
shutil.copy(tmp_dir / ".dvc" / "plots" / "default.json", template)
|
||||
template.write_text(content)
|
||||
return template
|
||||
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ class TmpDir(pathlib.Path):
|
||||
path.name: path.read_text(*args, **kwargs)
|
||||
for path in self.iterdir()
|
||||
}
|
||||
return super().read_text(*args, **kwargs)
|
||||
return super().read_text(*args, encoding="utf-8", **kwargs)
|
||||
|
||||
def hash_to_path_info(self, hash_):
|
||||
return self / hash_[0:2] / hash_[2:]
|
||||
|
||||
@@ -37,3 +37,14 @@ services:
|
||||
- 2222
|
||||
volumes:
|
||||
- ./remotes/user.key.pub:/tmp/key
|
||||
|
||||
git-server:
|
||||
image: ghcr.io/linuxserver/openssh-server
|
||||
environment:
|
||||
- USER_NAME=user
|
||||
- PUBLIC_KEY_FILE=/tmp/key
|
||||
ports:
|
||||
- 2222
|
||||
volumes:
|
||||
- ./remotes/user.key.pub:/tmp/key
|
||||
- ./remotes/git-init:/config/custom-cont-init.d
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from funcy import first
|
||||
|
||||
from dvc.dvcfile import PIPELINE_FILE
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.repo.experiments.utils import exp_refs_by_rev
|
||||
from dvc.utils.serialize import PythonFileCorruptedError
|
||||
from tests.func.test_repro_multistage import COPY_SCRIPT
|
||||
@@ -118,10 +119,6 @@ def test_failed_exp(tmp_dir, scm, dvc, exp_stage, mocker, caplog):
|
||||
["foo[1]=[baz, goo]"],
|
||||
"{foo: [bar: 1, [baz, goo]], goo: {bag: 3}, lorem: false}",
|
||||
],
|
||||
[
|
||||
["lorem.ipsum=3"],
|
||||
"{foo: [bar: 1, baz: 2], goo: {bag: 3}, lorem: {ipsum: 3}}",
|
||||
],
|
||||
],
|
||||
)
|
||||
def test_modify_params(tmp_dir, scm, dvc, mocker, changes, expected):
|
||||
@@ -148,6 +145,28 @@ def test_modify_params(tmp_dir, scm, dvc, mocker, changes, expected):
|
||||
assert fobj.read().strip() == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changes",
|
||||
[["lorem.ipsum=3"], ["foo[0].bazar=3"]],
|
||||
)
|
||||
def test_add_params(tmp_dir, scm, dvc, changes):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
tmp_dir.gen(
|
||||
"params.yaml", "{foo: [bar: 1, baz: 2], goo: {bag: 3}, lorem: false}"
|
||||
)
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py params.yaml metrics.yaml",
|
||||
metrics_no_cache=["metrics.yaml"],
|
||||
params=["foo", "goo", "lorem"],
|
||||
name="copy-file",
|
||||
)
|
||||
scm.add(["dvc.yaml", "dvc.lock", "copy.py", "params.yaml", "metrics.yaml"])
|
||||
scm.commit("init")
|
||||
|
||||
with pytest.raises(DvcException):
|
||||
dvc.experiments.run(stage.addressing, params=changes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("queue", [True, False])
|
||||
def test_apply(tmp_dir, scm, dvc, exp_stage, queue):
|
||||
from dvc.exceptions import InvalidArgumentError
|
||||
@@ -387,7 +406,7 @@ def test_no_scm(tmp_dir):
|
||||
@pytest.mark.parametrize("workspace", [True, False])
|
||||
def test_untracked(tmp_dir, scm, dvc, caplog, workspace):
|
||||
tmp_dir.gen("copy.py", COPY_SCRIPT)
|
||||
tmp_dir.gen("params.yaml", "foo: 1")
|
||||
tmp_dir.scm_gen("params.yaml", "foo: 1", commit="track params")
|
||||
stage = dvc.run(
|
||||
cmd="python copy.py params.yaml metrics.yaml",
|
||||
metrics_no_cache=["metrics.yaml"],
|
||||
|
||||
@@ -242,7 +242,7 @@ def test_push_pull_cache(
|
||||
hash_ = digest(str(x))
|
||||
path = os.path.join(local_remote.url, hash_[:2], hash_[2:])
|
||||
assert os.path.exists(path)
|
||||
assert open(path).read() == str(x)
|
||||
assert open(path, encoding="utf-8").read() == str(x)
|
||||
|
||||
remove(dvc.odb.local.cache_dir)
|
||||
|
||||
@@ -251,7 +251,7 @@ def test_push_pull_cache(
|
||||
hash_ = digest(str(x))
|
||||
path = os.path.join(dvc.odb.local.cache_dir, hash_[:2], hash_[2:])
|
||||
assert os.path.exists(path)
|
||||
assert open(path).read() == str(x)
|
||||
assert open(path, encoding="utf-8").read() == str(x)
|
||||
|
||||
|
||||
def test_auth_error_list(tmp_dir, scm, dvc, http_auth_patch):
|
||||
|
||||
@@ -477,7 +477,7 @@ def test_show_with_broken_repo(tmp_dir, scm, dvc, exp_stage, caplog):
|
||||
exp1 = dvc.experiments.run(exp_stage.addressing, params=["foo=2"])
|
||||
exp2 = dvc.experiments.run(exp_stage.addressing, params=["foo=3"])
|
||||
|
||||
with open("dvc.yaml", "a") as fd:
|
||||
with open("dvc.yaml", "a", encoding="utf-8") as fd:
|
||||
fd.write("breaking the yaml!")
|
||||
|
||||
result = dvc.experiments.show()
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import textwrap
|
||||
|
||||
CONFIG_TEXT = textwrap.dedent(
|
||||
"""\
|
||||
[feature]
|
||||
machine = true
|
||||
['machine "foo"']
|
||||
cloud = aws
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -2,17 +2,11 @@ import os
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
import tpi
|
||||
|
||||
from dvc.main import main
|
||||
|
||||
CONFIG_TEXT = textwrap.dedent(
|
||||
"""\
|
||||
[feature]
|
||||
machine = true
|
||||
['machine \"foo\"']
|
||||
cloud = aws
|
||||
"""
|
||||
)
|
||||
from . import CONFIG_TEXT
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -121,3 +115,62 @@ def test_machine_list(tmp_dir, dvc, capsys):
|
||||
)
|
||||
in cap.out
|
||||
)
|
||||
|
||||
|
||||
def test_machine_rename_success(tmp_dir, scm, dvc, capsys, mocker):
|
||||
config_file = tmp_dir / ".dvc" / "config"
|
||||
config_file.write_text(CONFIG_TEXT)
|
||||
|
||||
mocker.patch.object(
|
||||
tpi.terraform.TerraformBackend,
|
||||
"state_mv",
|
||||
autospec=True,
|
||||
return_value=True,
|
||||
)
|
||||
|
||||
os.makedirs((tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "foo"))
|
||||
|
||||
assert main(["machine", "rename", "foo", "bar"]) == 0
|
||||
cap = capsys.readouterr()
|
||||
assert "Rename machine 'foo' to 'bar'." in cap.out
|
||||
assert config_file.read_text() == CONFIG_TEXT.replace("foo", "bar")
|
||||
assert not (
|
||||
tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "foo"
|
||||
).exists()
|
||||
assert (
|
||||
tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "bar"
|
||||
).exists()
|
||||
|
||||
|
||||
def test_machine_rename_none_exist(tmp_dir, scm, dvc, caplog):
|
||||
config_alice = CONFIG_TEXT.replace("foo", "alice")
|
||||
config_file = tmp_dir / ".dvc" / "config"
|
||||
config_file.write_text(config_alice)
|
||||
assert main(["machine", "rename", "foo", "bar"]) == 251
|
||||
assert config_file.read_text() == config_alice
|
||||
assert "machine 'foo' doesn't exist." in caplog.text
|
||||
|
||||
|
||||
def test_machine_rename_exist(tmp_dir, scm, dvc, caplog):
|
||||
config_bar = CONFIG_TEXT + "['machine \"bar\"']\n cloud = aws"
|
||||
config_file = tmp_dir / ".dvc" / "config"
|
||||
config_file.write_text(config_bar)
|
||||
assert main(["machine", "rename", "foo", "bar"]) == 251
|
||||
assert config_file.read_text() == config_bar
|
||||
assert "Machine 'bar' already exists." in caplog.text
|
||||
|
||||
|
||||
def test_machine_rename_error(tmp_dir, scm, dvc, caplog, mocker):
|
||||
config_file = tmp_dir / ".dvc" / "config"
|
||||
config_file.write_text(CONFIG_TEXT)
|
||||
|
||||
os.makedirs((tmp_dir / ".dvc" / "tmp" / "machine" / "terraform" / "foo"))
|
||||
|
||||
def cmd_error(self, source, destination, **kwargs):
|
||||
raise tpi.TPIError("test error")
|
||||
|
||||
mocker.patch.object(tpi.terraform.TerraformBackend, "state_mv", cmd_error)
|
||||
|
||||
assert main(["machine", "rename", "foo", "bar"]) == 251
|
||||
assert config_file.read_text() == CONFIG_TEXT
|
||||
assert "rename failed" in caplog.text
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import json
|
||||
|
||||
from tpi.terraform import TerraformBackend
|
||||
|
||||
from dvc.command.machine import CmdMachineStatus
|
||||
from dvc.main import main
|
||||
|
||||
from . import CONFIG_TEXT
|
||||
|
||||
STATUS = """{
|
||||
"aws_security_group": null,
|
||||
"cloud": "aws",
|
||||
"id": "iterative-2jyhw8j9ieov6",
|
||||
"image": "ubuntu-bionic-18.04-amd64-server-20210818",
|
||||
"instance_gpu": null,
|
||||
"instance_hdd_size": 35,
|
||||
"instance_ip": "123.123.123.123",
|
||||
"instance_launch_time": "2021-08-25T07:13:03Z",
|
||||
"instance_type": "m",
|
||||
"name": "test-resource",
|
||||
"region": "us-west",
|
||||
"spot": false,
|
||||
"spot_price": -1,
|
||||
"ssh_name": null,
|
||||
"ssh_private": "-----BEGIN RSA PRIVATE KEY-----\\n",
|
||||
"startup_script": "IyEvYmluL2Jhc2g=",
|
||||
"timeouts": null
|
||||
}"""
|
||||
|
||||
|
||||
def test_status(tmp_dir, scm, dvc, mocker, capsys):
|
||||
(tmp_dir / ".dvc" / "config").write_text(CONFIG_TEXT)
|
||||
status = json.loads(STATUS)
|
||||
|
||||
mocker.patch.object(
|
||||
TerraformBackend, "instances", autospec=True, return_value=[status]
|
||||
)
|
||||
|
||||
assert main(["machine", "status", "foo"]) == 0
|
||||
cap = capsys.readouterr()
|
||||
assert "instance_num_1:" in cap.out
|
||||
for key in CmdMachineStatus.SHOWN_FIELD:
|
||||
assert f"\t{key:20}: {status[key]}" in cap.out
|
||||
@@ -187,9 +187,10 @@ def test_metrics_diff_cli(tmp_dir, scm, dvc, run_copy_metrics, caplog, capsys):
|
||||
assert main(["metrics", "diff", "HEAD~2"]) == 0
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == (
|
||||
"Path Metric Old New Change\n"
|
||||
"m.yaml foo 1.23457 3.45679 2.22222\n"
|
||||
"Path Metric HEAD~2 workspace Change\n"
|
||||
"m.yaml foo 1.23457 3.45679 2.22222\n"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ def test_log_errors(
|
||||
)
|
||||
scm.tag("v1")
|
||||
|
||||
with open(file, "a") as fd:
|
||||
with open(file, "a", encoding="utf-8") as fd:
|
||||
fd.write("\nMALFORMED!")
|
||||
|
||||
result = dvc.metrics.show(revs=["v1"])
|
||||
|
||||
@@ -16,7 +16,7 @@ def index(dvc, local_remote, mocker):
|
||||
def test_indexed_on_status(tmp_dir, dvc, index):
|
||||
foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0]
|
||||
bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0]
|
||||
baz_hash = bar.obj.trie.get(("baz",)).hash_info
|
||||
baz_hash = bar.obj.trie.get(("baz",))[1]
|
||||
clean_staging()
|
||||
dvc.push()
|
||||
index.clear()
|
||||
@@ -30,7 +30,7 @@ def test_indexed_on_status(tmp_dir, dvc, index):
|
||||
def test_indexed_on_push(tmp_dir, dvc, index):
|
||||
foo = tmp_dir.dvc_gen({"foo": "foo content"})[0].outs[0]
|
||||
bar = tmp_dir.dvc_gen({"bar": {"baz": "baz content"}})[0].outs[0]
|
||||
baz_hash = bar.obj.trie.get(("baz",)).hash_info
|
||||
baz_hash = bar.obj.trie.get(("baz",))[1]
|
||||
clean_staging()
|
||||
|
||||
dvc.push()
|
||||
@@ -60,8 +60,8 @@ def test_clear_on_download_err(tmp_dir, dvc, index, mocker):
|
||||
out = tmp_dir.dvc_gen({"dir": {"foo": "foo content"}})[0].outs[0]
|
||||
dvc.push()
|
||||
|
||||
for _, entry in out.obj:
|
||||
remove(dvc.odb.local.get(entry.hash_info).path_info)
|
||||
for _, _, oid in out.obj:
|
||||
remove(dvc.odb.local.get(oid).path_info)
|
||||
remove(out.path_info)
|
||||
|
||||
assert list(index.hashes())
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_log_errors(tmp_dir, scm, dvc, capsys, file, error_path):
|
||||
)
|
||||
|
||||
rename = (tmp_dir / file).read_text()
|
||||
with open(tmp_dir / file, "a") as fd:
|
||||
with open(tmp_dir / file, "a", encoding="utf-8") as fd:
|
||||
fd.write("\nmalformed!")
|
||||
|
||||
scm.add([PIPELINE_FILE, "params_other.yaml"])
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_plot_wrong_metric_type(tmp_dir, scm, dvc, run_copy_metrics):
|
||||
)
|
||||
|
||||
assert isinstance(
|
||||
dvc.plots.collect(targets=["metric.txt"], onerror=onerror_collect)[
|
||||
dvc.plots.show(targets=["metric.txt"], onerror=onerror_collect)[
|
||||
"workspace"
|
||||
]["data"]["metric.txt"]["error"],
|
||||
PlotMetricTypeError,
|
||||
@@ -148,7 +148,7 @@ def test_plots_show_overlap(tmp_dir, dvc, run_copy_metrics, clear_before_run):
|
||||
dvc._reset()
|
||||
|
||||
assert isinstance(
|
||||
dvc.plots.collect(onerror=onerror_collect)["workspace"]["error"],
|
||||
dvc.plots.show(onerror=onerror_collect)["workspace"]["error"],
|
||||
OverlappingOutputPathsError,
|
||||
)
|
||||
|
||||
@@ -183,12 +183,12 @@ def test_dir_plots(tmp_dir, dvc, run_copy_metrics):
|
||||
assert result["workspace"]["data"][p2]["props"] == props
|
||||
|
||||
|
||||
def test_ignore_binary_file(tmp_dir, dvc, run_copy_metrics):
|
||||
with open("file", "wb") as fobj:
|
||||
def test_ignore_parsing_error(tmp_dir, dvc, run_copy_metrics):
|
||||
with open("file", "wb", encoding=None) as fobj:
|
||||
fobj.write(b"\xc1")
|
||||
|
||||
run_copy_metrics("file", "plot_file.json", plots=["plot_file.json"])
|
||||
result = dvc.plots.collect(onerror=onerror_collect)
|
||||
result = dvc.plots.show(onerror=onerror_collect)
|
||||
|
||||
assert isinstance(
|
||||
result["workspace"]["data"]["plot_file.json"]["error"], EncodingError
|
||||
@@ -216,10 +216,10 @@ def test_log_errors(
|
||||
)
|
||||
scm.tag("v1")
|
||||
|
||||
with open(file, "a") as fd:
|
||||
with open(file, "a", encoding="utf-8") as fd:
|
||||
fd.write("\nMALFORMED!")
|
||||
|
||||
result = dvc.plots.collect(onerror=onerror_collect)
|
||||
result = dvc.plots.show(onerror=onerror_collect)
|
||||
_, error = capsys.readouterr()
|
||||
|
||||
assert isinstance(get_in(result, error_path), YAMLFileCorruptedError)
|
||||
|
||||
+16
-11
@@ -87,7 +87,7 @@ def test_add_executable(tmp_dir, dvc):
|
||||
|
||||
|
||||
def test_add_unicode(tmp_dir, dvc):
|
||||
with open("\xe1", "wb") as fd:
|
||||
with open("\xe1", "wb", encoding=None) as fd:
|
||||
fd.write(b"something")
|
||||
|
||||
(stage,) = dvc.add("\xe1")
|
||||
@@ -112,7 +112,7 @@ def test_add_directory(tmp_dir, dvc):
|
||||
hash_info = stage.outs[0].hash_info
|
||||
|
||||
obj = load(dvc.odb.local, hash_info)
|
||||
for key, _ in obj:
|
||||
for key, _, _ in obj:
|
||||
for part in key:
|
||||
assert "\\" not in part
|
||||
|
||||
@@ -143,7 +143,7 @@ class TestAddCmdDirectoryRecursive(TestDvc):
|
||||
# Create a lot of files
|
||||
for iteration in range(LARGE_DIR_SIZE + 1):
|
||||
path = os.path.join("large-dir", str(iteration))
|
||||
with open(path, "w") as fobj:
|
||||
with open(path, "w", encoding="utf-8") as fobj:
|
||||
fobj.write(path)
|
||||
|
||||
assert main(["add", "--recursive", "large-dir"]) == 0
|
||||
@@ -534,7 +534,7 @@ class TestAddCommit(TestDvc):
|
||||
|
||||
def test_should_collect_dir_cache_only_once(mocker, tmp_dir, dvc):
|
||||
tmp_dir.gen({"data/data": "foo"})
|
||||
counter = mocker.spy(dvc_module.objects.stage, "_get_tree_obj")
|
||||
counter = mocker.spy(dvc_module.objects.stage, "_stage_tree")
|
||||
ret = main(["add", "data"])
|
||||
assert ret == 0
|
||||
assert counter.mock.call_count == 2
|
||||
@@ -581,7 +581,7 @@ class TestShouldThrowProperExceptionOnCorruptedStageFile(TestDvc):
|
||||
foo_stage = relpath(self.FOO + DVC_FILE_SUFFIX)
|
||||
|
||||
# corrupt stage file
|
||||
with open(foo_stage, "a+") as file:
|
||||
with open(foo_stage, "a+", encoding="utf-8") as file:
|
||||
file.write("this will break yaml file structure")
|
||||
|
||||
self._caplog.clear()
|
||||
@@ -677,8 +677,12 @@ def temporary_windows_drive(tmp_path_factory):
|
||||
import string
|
||||
from ctypes import windll
|
||||
|
||||
import win32api # pylint: disable=import-error
|
||||
from win32con import DDD_REMOVE_DEFINITION # pylint: disable=import-error
|
||||
try:
|
||||
# pylint: disable=import-error
|
||||
import win32api
|
||||
from win32con import DDD_REMOVE_DEFINITION
|
||||
except ImportError:
|
||||
pytest.skip("pywin32 not installed")
|
||||
|
||||
drives = [
|
||||
s[0].upper()
|
||||
@@ -999,7 +1003,7 @@ def test_add_preserve_meta(tmp_dir, dvc):
|
||||
# are the same 260 chars, which makes the test unnecessarily complex
|
||||
@pytest.mark.skipif(os.name == "nt", reason="unsupported on Windows")
|
||||
def test_add_long_fname(tmp_dir, dvc):
|
||||
name_max = os.pathconf(tmp_dir, "PC_NAME_MAX")
|
||||
name_max = os.pathconf(tmp_dir, "PC_NAME_MAX") # pylint: disable=no-member
|
||||
name = "a" * name_max
|
||||
tmp_dir.gen({"data": {name: "foo"}})
|
||||
|
||||
@@ -1026,8 +1030,9 @@ def test_add_to_remote(tmp_dir, dvc, local_cloud, local_remote):
|
||||
assert len(stage.outs) == 1
|
||||
|
||||
hash_info = stage.outs[0].hash_info
|
||||
meta = stage.outs[0].meta
|
||||
assert local_remote.hash_to_path_info(hash_info.value).read_text() == "foo"
|
||||
assert hash_info.size == len("foo")
|
||||
assert meta.size == len("foo")
|
||||
|
||||
|
||||
def test_add_to_remote_absolute(tmp_dir, make_tmp_dir, dvc, local_remote):
|
||||
@@ -1071,8 +1076,8 @@ def test_add_to_cache_dir(tmp_dir, dvc, local_cloud):
|
||||
(stage,) = dvc.add(str(local_cloud / "data"), out="data")
|
||||
assert len(stage.deps) == 0
|
||||
assert len(stage.outs) == 1
|
||||
assert stage.outs[0].hash_info.size == len("foo") + len("bar")
|
||||
assert stage.outs[0].hash_info.nfiles == 2
|
||||
assert stage.outs[0].meta.size == len("foo") + len("bar")
|
||||
assert stage.outs[0].meta.nfiles == 2
|
||||
|
||||
data = tmp_dir / "data"
|
||||
assert data.read_text() == {"foo": "foo", "bar": "bar"}
|
||||
|
||||
@@ -73,7 +73,7 @@ class TestCheckoutCorruptedCacheFile(TestRepro):
|
||||
cache = self.foo_stage.outs[0].cache_path
|
||||
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "a") as fd:
|
||||
with open(cache, "a", encoding="utf-8") as fd:
|
||||
fd.write("1")
|
||||
|
||||
with pytest.raises(CheckoutError):
|
||||
@@ -101,13 +101,13 @@ class TestCheckoutCorruptedCacheDir(TestDvc):
|
||||
# NOTE: modifying cache file for one of the files inside the directory
|
||||
# to check if dvc will detect that the cache is corrupted.
|
||||
obj = load(self.dvc.odb.local, out.hash_info)
|
||||
_, entry_obj = list(obj)[0]
|
||||
_, _, entry_oid = list(obj)[0]
|
||||
cache = os.fspath(
|
||||
self.dvc.odb.local.hash_to_path_info(entry_obj.hash_info.value)
|
||||
self.dvc.odb.local.hash_to_path_info(entry_oid.value)
|
||||
)
|
||||
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "w+") as fobj:
|
||||
with open(cache, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("1")
|
||||
|
||||
with pytest.raises(CheckoutError):
|
||||
@@ -127,7 +127,7 @@ class CheckoutBase(TestDvcGit):
|
||||
GIT_IGNORE = ".gitignore"
|
||||
|
||||
def commit_data_file(self, fname, content="random text"):
|
||||
with open(fname, "w") as fd:
|
||||
with open(fname, "w", encoding="utf-8") as fd:
|
||||
fd.write(content)
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
@@ -136,7 +136,7 @@ class CheckoutBase(TestDvcGit):
|
||||
self.dvc.scm.commit("adding " + fname)
|
||||
|
||||
def read_ignored(self):
|
||||
with open(self.GIT_IGNORE) as f:
|
||||
with open(self.GIT_IGNORE, encoding="utf-8") as f:
|
||||
return [s.strip("\n") for s in f.readlines()]
|
||||
|
||||
def outs_info(self, stage):
|
||||
@@ -188,7 +188,7 @@ class TestCheckoutCleanWorkingDir(CheckoutBase):
|
||||
stage = stages[0]
|
||||
|
||||
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
|
||||
with open(working_dir_change, "w") as f:
|
||||
with open(working_dir_change, "w", encoding="utf-8") as f:
|
||||
f.write("not_cached")
|
||||
|
||||
ret = main(["checkout", stage.relpath])
|
||||
@@ -204,7 +204,7 @@ class TestCheckoutCleanWorkingDir(CheckoutBase):
|
||||
stage = stages[0]
|
||||
|
||||
working_dir_change = os.path.join(self.DATA_DIR, "not_cached.txt")
|
||||
with open(working_dir_change, "w") as f:
|
||||
with open(working_dir_change, "w", encoding="utf-8") as f:
|
||||
f.write("not_cached")
|
||||
|
||||
ret = main(["checkout", stage.relpath])
|
||||
@@ -610,7 +610,7 @@ def test_checkout_stats_on_failure(tmp_dir, dvc, scm):
|
||||
# corrupt cache
|
||||
cache = stage.outs[0].cache_path
|
||||
os.chmod(cache, 0o644)
|
||||
with open(cache, "a") as fd:
|
||||
with open(cache, "a", encoding="utf-8") as fd:
|
||||
fd.write("destroy cache")
|
||||
|
||||
scm.checkout("HEAD~")
|
||||
|
||||
@@ -57,9 +57,7 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
|
||||
out_dir = stage_dir.outs[0]
|
||||
cache_dir = out_dir.cache_path
|
||||
dir_hash = out_dir.hash_info
|
||||
dir_hashes = {dir_hash} | {
|
||||
entry_obj.hash_info for _, entry_obj in out_dir.obj
|
||||
}
|
||||
dir_hashes = {dir_hash} | {oid for _, _, oid in out_dir.obj}
|
||||
|
||||
def _check_status(status, **kwargs):
|
||||
for key in ("ok", "missing", "new", "deleted"):
|
||||
@@ -114,7 +112,7 @@ def test_cloud(tmp_dir, dvc, remote): # pylint:disable=unused-argument
|
||||
dvc.cloud.pull(foo_hashes)
|
||||
assert os.path.exists(cache)
|
||||
assert os.path.isfile(cache)
|
||||
with open(cache) as fd:
|
||||
with open(cache, encoding="utf-8") as fd:
|
||||
assert fd.read() == "foo"
|
||||
|
||||
dvc.cloud.pull(dir_hashes)
|
||||
@@ -168,7 +166,7 @@ def test_cloud_cli(tmp_dir, dvc, remote):
|
||||
assert os.path.isfile("foo")
|
||||
assert os.path.isdir("data_dir")
|
||||
|
||||
with open(cache) as fd:
|
||||
with open(cache, encoding="utf-8") as fd:
|
||||
assert fd.read() == "foo"
|
||||
assert os.path.isfile(cache_dir)
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ def test_subrepos_are_ignored(tmp_dir, erepo_dir):
|
||||
clean_staging()
|
||||
makedirs(cache_dir)
|
||||
|
||||
staging, obj = stage(
|
||||
staging, _, obj = stage(
|
||||
repo.odb.local,
|
||||
PathInfo(repo.root_dir) / "dir",
|
||||
repo.repo_fs,
|
||||
|
||||
@@ -60,7 +60,7 @@ class TestGCBranchesTags(TestDvcGit):
|
||||
def test(self):
|
||||
fname = "file"
|
||||
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("v1.0")
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
@@ -71,7 +71,7 @@ class TestGCBranchesTags(TestDvcGit):
|
||||
|
||||
self.dvc.scm.checkout("test", create_new=True)
|
||||
self.dvc.remove(stages[0].relpath)
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("test")
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
@@ -80,7 +80,7 @@ class TestGCBranchesTags(TestDvcGit):
|
||||
|
||||
self.dvc.scm.checkout("master")
|
||||
self.dvc.remove(stages[0].relpath)
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("trash")
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
@@ -88,7 +88,7 @@ class TestGCBranchesTags(TestDvcGit):
|
||||
self.dvc.scm.commit("trash")
|
||||
|
||||
self.dvc.remove(stages[0].relpath)
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("master")
|
||||
stages = self.dvc.add(fname)
|
||||
self.assertEqual(len(stages), 1)
|
||||
@@ -138,7 +138,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
|
||||
|
||||
# ADD FILE ONLY IN MAIN PROJECT
|
||||
fname = "only_in_first"
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("only in main repo")
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
@@ -146,7 +146,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
|
||||
|
||||
# ADD FILE IN MAIN PROJECT THAT IS ALSO IN SECOND PROJECT
|
||||
fname = "in_both"
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("in both repos")
|
||||
|
||||
stages = self.dvc.add(fname)
|
||||
@@ -156,7 +156,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
|
||||
os.chdir(self.additional_path)
|
||||
# ADD FILE ONLY IN SECOND PROJECT
|
||||
fname = "only_in_second"
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("only in additional repo")
|
||||
|
||||
stages = self.additional_dvc.add(fname)
|
||||
@@ -164,7 +164,7 @@ class TestGCMultipleDvcRepos(TestDvcGit):
|
||||
|
||||
# ADD FILE IN SECOND PROJECT THAT IS ALSO IN MAIN PROJECT
|
||||
fname = "in_both"
|
||||
with open(fname, "w+") as fobj:
|
||||
with open(fname, "w+", encoding="utf-8") as fobj:
|
||||
fobj.write("in both repos")
|
||||
|
||||
stages = self.additional_dvc.add(fname)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user