Compare commits

...
13 Commits
Author SHA1 Message Date
Ruslan Kuprieiev 58a5442d71 dvc: bump to 2.0.8 2021-03-24 19:20:04 +02:00
Saugat Pachhai 76ece8c64a plots: quote and resolve paths properly (#5670)
This commit fixes 3 things:
1. The path printed on the terminal is properly is quoted.  This was
problematic when the output is piped to `xdg-open` if the path contained
spaces or quotes or special characters.

2. The path is resolved. This is not an issue at all, but makes the
output nicer.

3. The path with which the browser is opened with uses relative path.
This is done to solve issue in WSL, that the absolute path is not
accessible outside the WSL for host programs as the linux filesystem
is available on `/wsl$` path.
2021-03-24 20:27:24 +05:45
Saugat Pachhai 8b6d655ebe use global UI object for printing (#5653) 2021-03-24 17:22:17 +05:45
Ruslan Kuprieiev f2ba67aece build: upgrade pyinstaller to 4.2 (#5682)
* build: upgrade pyinstaller to 4.2

* pyinstaller: hooks: remove hook-googleapiclient.model.py

https://github.com/pyinstaller/pyinstaller-hooks-contrib/blob/master/src/_pyinstaller_hooks_contrib/hooks/stdhooks/hook-googleapiclient.model.py

* gha: use python 3.8 for pyinstaller
2021-03-24 09:43:05 +02:00
Ruslan Kuprieiev 19cdef1675 build: separate building and packaging (#5675)
* build: separate pyinstaller and fpm/innosetup

* gha: upload deb/rpm/osxpkg/exe artifacts

* build: dynamically generate dvc version
2021-03-23 19:55:44 +02:00
Saugat Pachhai dc3fe6fbec Update pylint-pytest to 1.0.3 (#5673) 2021-03-23 15:28:33 +05:45
Ruslan Kuprieiev 619c55d399 dvc: bump to 2.0.7 2021-03-22 15:29:52 +02:00
Alex Ford b3bfa346c3 Using existing logging level if defined in addLoggingLevel (#5632)
* Use existing level if defined in addLoggingLevel

Fixes https://github.com/iterative/dvc/issues/4546

* Add addLoggingLevel test

* Format fix.
2021-03-22 15:15:16 +02:00
Saugat Pachhai 0a8927e3d8 diff: clear calculated renamed files from inverted hashmap (#5652)
* diff: clear calculated renamed files from inverted hashmap

We were trying to reuse the first encounter of file from inverted hashmap again and again, which was throwing ValueError

* refactor

* remove ordering

* add tests
2021-03-22 15:13:52 +02:00
Batuhan Taskaya 35597ed83d fsspec: skip instance cache on all filesystems (#5668) 2021-03-22 14:36:21 +02:00
Batuhan Taskaya 21fc06e947 fsspec: support raw buckets on path splitting (#5658) 2021-03-22 02:01:06 +02:00
Saugat Pachhai f2b216261e move Repo.diff related test to diff (#5654) 2021-03-19 16:58:21 +05:45
Batuhan Taskaya 6026573860 update: use unresolved path with --to-remote calls (#5631) 2021-03-17 16:48:00 +02:00
39 changed files with 644 additions and 425 deletions
+74 -18
View File
@@ -6,20 +6,30 @@ on:
release:
types: [released, prereleased]
jobs:
linux:
deb:
runs-on: ubuntu-16.04
steps:
- uses: actions/[email protected]
- name: Set up Python 3.7
- name: Set up Python 3.8
uses: actions/[email protected]
with:
python-version: 3.7
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/setup-ruby@v1
with:
ruby-version: 2.4
- name: Build
run: ./scripts/build_posix.sh
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build deb
run: python scripts/build.py deb
- name: Upload deb artifact
uses: actions/upload-artifact@v2
with:
path: scripts/fpm/dvc*.deb
if-no-files-found: error
- name: Publish deb
if: github.event_name == 'release'
uses: actions/[email protected]
@@ -27,9 +37,33 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_path: scripts/fpm/dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_name: dvc_${{ github.event.release.tag_name }}_amd64.deb
asset_content_type: binary/octet-stream
rpm:
runs-on: ubuntu-16.04
steps:
- uses: actions/[email protected]
- name: Set up Python 3.8
uses: actions/[email protected]
with:
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/setup-ruby@v1
with:
ruby-version: 2.4
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build rpm
run: python scripts/build.py rpm
- name: Upload rpm artifact
uses: actions/upload-artifact@v2
with:
path: scripts/fpm/dvc*.rpm
if-no-files-found: error
- name: Publish rpm
if: github.event_name == 'release'
uses: actions/[email protected]
@@ -37,23 +71,33 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_name: dvc-${{ github.event.release.tag_name }}-1.x86_64.rpm
asset_content_type: binary/octet-stream
mac:
osxpkg:
runs-on: macos-10.15
steps:
- uses: actions/[email protected]
- name: Set up Python 3.7
- name: Set up Python 3.8
uses: actions/[email protected]
with:
python-version: 3.7
python-version: 3.8
- name: Set up Ruby 2.4
uses: actions/setup-ruby@v1
with:
ruby-version: 2.4
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
gem install --no-document fpm
- name: Build
run: ./scripts/build_posix.sh
run: python scripts/build.py osxpkg
- name: Upload osxpkg artifact
uses: actions/upload-artifact@v2
with:
path: scripts/fpm/dvc*.pkg
if-no-files-found: error
- name: Publish
if: github.event_name == 'release'
uses: actions/[email protected]
@@ -61,20 +105,32 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc-${{ github.event.release.tag_name }}.pkg
asset_path: scripts/fpm/dvc-${{ github.event.release.tag_name }}.pkg
asset_name: dvc-${{ github.event.release.tag_name }}.pkg
asset_content_type: binary/octet-stream
windows:
exe:
runs-on: windows-2019
steps:
- uses: actions/[email protected]
- name: Set up Python 3.7
- name: Set up Python 3.8
uses: actions/[email protected]
with:
python-version: 3.7
python-version: 3.8
- name: Install deps
run: |
pip install .[all]
pip install -r scripts/build-requirements.txt
- name: Pull images
run: |
dvc pull
- name: Build
run: scripts\build_windows.cmd
run: python scripts\build.py exe
shell: cmd
- name: Upload exe artifact
uses: actions/upload-artifact@v2
with:
path: scripts/innosetup/dvc*.exe
if-no-files-found: error
- name: Publish
if: github.event_name == 'release'
env:
@@ -82,10 +138,10 @@ jobs:
uses: actions/[email protected]
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: dvc-${{ github.event.release.tag_name }}.exe
asset_path: scripts/innosetup/dvc-${{ github.event.release.tag_name }}.exe
asset_name: dvc-${{ github.event.release.tag_name }}.exe
asset_content_type: binary/octet-stream
python:
pip:
runs-on: ubuntu-16.04
steps:
- uses: actions/[email protected]
+7 -6
View File
@@ -1,6 +1,5 @@
import argparse
import logging
import os
from dvc.command import completion
from dvc.command.base import CmdBase, append_doc_link, fix_subparsers
@@ -23,6 +22,8 @@ class CmdPlots(CmdBase):
return {k: v for k, v in props.items() if v is not None}
def run(self):
from pathlib import Path
if self.args.show_vega:
if not self.args.targets:
logger.error("please specify a target for `--show-vega`")
@@ -41,20 +42,20 @@ class CmdPlots(CmdBase):
logger.info(plots[target])
return 0
path = self.args.out or "plots.html"
path = os.path.join(os.getcwd(), path)
rel: str = self.args.out or "plots.html"
path = (Path.cwd() / rel).resolve()
write(path, plots)
url = f"file://{path}"
except DvcException:
logger.exception("")
return 1
assert path.is_absolute() # as_uri throws ValueError if not absolute
url = path.as_uri()
logger.info(url)
if self.args.open:
import webbrowser
opened = webbrowser.open(url)
opened = webbrowser.open(rel)
if not opened:
logger.error("Failed to open. Please try opening it manually.")
return 1
+5 -3
View File
@@ -44,6 +44,8 @@ class CmdDataStatus(CmdDataBase):
self._show(value, indent + 1)
def run(self):
from dvc.ui import ui
indent = 1 if self.args.cloud else 0
try:
st = self.repo.status(
@@ -68,14 +70,14 @@ class CmdDataStatus(CmdDataBase):
elif st:
self._show(st, indent)
elif not self.repo.stages:
logger.info(self.EMPTY_PROJECT_MSG)
ui.write(self.EMPTY_PROJECT_MSG)
elif self.args.cloud or self.args.remote:
remote = self.args.remote or self.repo.config["core"].get(
"remote"
)
logger.info(self.IN_SYNC_MSG.format(remote=remote))
ui.write(self.IN_SYNC_MSG.format(remote=remote))
else:
logger.info(self.UP_TO_DATE_MSG)
ui.write(self.UP_TO_DATE_MSG)
except DvcException:
logger.exception("")
+3 -4
View File
@@ -48,8 +48,6 @@ class AzureFileSystem(FSSpecWrapper):
self.path_info = self.PATH_CLS(url)
self.bucket = self.path_info.bucket
self.login_method, self.login_info = self._prepare_credentials(config)
def _prepare_credentials(self, config):
from azure.identity.aio import DefaultAzureCredential
@@ -90,7 +88,8 @@ class AzureFileSystem(FSSpecWrapper):
else:
login_method = None
return login_method, login_info
self.login_method = login_method
return login_info
@cached_property
def _az_config(self):
@@ -111,7 +110,7 @@ class AzureFileSystem(FSSpecWrapper):
from azure.core.exceptions import AzureError
try:
file_system = AzureBlobFileSystem(**self.login_info)
file_system = AzureBlobFileSystem(**self.fs_args)
if self.bucket not in [
container.rstrip("/") for container in file_system.ls("/")
]:
+18 -2
View File
@@ -9,6 +9,11 @@ from .base import BaseFileSystem
# pylint: disable=no-member
class FSSpecWrapper(BaseFileSystem):
def __init__(self, repo, config):
super().__init__(repo, config)
self.fs_args = {"skip_instance_cache": True}
self.fs_args.update(self._prepare_credentials(config))
def fs(self):
raise NotImplementedError
@@ -19,8 +24,14 @@ class FSSpecWrapper(BaseFileSystem):
return path
def _strip_bucket(self, entry):
_, entry = entry.split("/", 1)
return entry
try:
bucket, path = entry.split("/", 1)
except ValueError:
# If there is no path attached, only returns
# the bucket (top-level).
bucket, path = entry, None
return path or bucket
def _strip_buckets(self, entries, detail, prefix=None):
for entry in entries:
@@ -38,6 +49,11 @@ class FSSpecWrapper(BaseFileSystem):
entries within info() and ls(detail=True) calls"""
return entry
def _prepare_credentials(self, config): # pylint: disable=unused-argument
"""Prepare the arguments for authentication to the
host filesystem"""
return {}
def isdir(self, path_info):
if not self.exists(path_info):
return False
+2 -4
View File
@@ -22,10 +22,8 @@ class GSFileSystem(FSSpecWrapper):
url = config.get("url", "gs://")
self.path_info = self.PATH_CLS(url)
self.login_info = self._prepare_credentials(config)
def _prepare_credentials(self, config):
login_info = {}
login_info = {"consistency": None}
login_info["project"] = config.get("projectname")
login_info["token"] = config.get("credentialpath")
return login_info
@@ -40,4 +38,4 @@ class GSFileSystem(FSSpecWrapper):
def fs(self):
from gcsfs import GCSFileSystem
return GCSFileSystem(**self.login_info, consistency=None)
return GCSFileSystem(**self.fs_args)
+21 -7
View File
@@ -23,14 +23,19 @@ def addLoggingLevel(levelName, levelNum, methodName=None):
Adds a new logging level to the `logging` module and the
currently configured logging class.
Uses the existing numeric levelNum if already defined.
Based on https://stackoverflow.com/questions/2183233
"""
if methodName is None:
methodName = levelName.lower()
assert not hasattr(logging, levelName)
assert not hasattr(logging, methodName)
assert not hasattr(logging.getLoggerClass(), methodName)
# If the level name is already defined as a top-level `logging`
# constant, then adopt the existing numeric level.
if hasattr(logging, levelName):
existingLevelNum = getattr(logging, levelName)
assert isinstance(existingLevelNum, int)
levelNum = existingLevelNum
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
@@ -40,10 +45,19 @@ def addLoggingLevel(levelName, levelNum, methodName=None):
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
logging.addLevelName(levelNum, levelName)
setattr(logging, levelName, levelNum)
setattr(logging.getLoggerClass(), methodName, logForLevel)
setattr(logging, methodName, logToRoot)
# getLevelName resolves the numeric log level if already defined,
# otherwise returns a string
if not isinstance(logging.getLevelName(levelName), int):
logging.addLevelName(levelNum, levelName)
if not hasattr(logging, levelName):
setattr(logging, levelName, levelNum)
if not hasattr(logging.getLoggerClass(), methodName):
setattr(logging.getLoggerClass(), methodName, logForLevel)
if not hasattr(logging, methodName):
setattr(logging, methodName, logToRoot)
class LoggingException(Exception):
+5
View File
@@ -45,6 +45,11 @@ def main(argv=None): # noqa: C901
logger.trace(args)
if not args.quiet:
from dvc.ui import ui
ui.enable()
with debugtools(args):
cmd = args.func(args)
ret = cmd.run()
+11 -15
View File
@@ -1,5 +1,7 @@
import logging
import os
from collections import defaultdict
from typing import Dict, List
from dvc.exceptions import PathMissingError
from dvc.objects.stage import get_file_hash
@@ -190,29 +192,23 @@ def _targets_to_path_infos(repo_fs, targets):
def _calculate_renamed(new, old, added, deleted):
old_inverted = {}
old_inverted: Dict[str, List[str]] = defaultdict(list)
# It is needed to be dict of lists to cover cases
# when repo has paths with same hash
for path, path_hash in old.items():
bucket = old_inverted.get(path_hash, None)
if bucket is None:
old_inverted[path_hash] = [path]
else:
bucket.append(path)
old_inverted[path_hash].append(path)
renamed = []
for path in added:
path_hash = new[path]
old_paths = old_inverted.get(path_hash, None)
if not old_paths:
continue
old_path = None
for tmp_old_path in old_paths:
if tmp_old_path in deleted:
old_path = tmp_old_path
break
if not old_path:
old_paths = old_inverted[path_hash]
try:
iterator = enumerate(old_paths)
index = next(idx for idx, path in iterator if path in deleted)
except StopIteration:
continue
old_path = old_paths.pop(index)
renamed.append(
{"path": {"old": old_path, "new": path}, "hash": path_hash}
)
+1 -1
View File
@@ -12,7 +12,7 @@ def _update_import_on_remote(stage, remote, jobs):
"be updated with --to-remote"
)
url = stage.deps[0].path_info.url
url = stage.deps[0].def_path
stage.outs[0].hash_info = stage.repo.cloud.transfer(
url, jobs=jobs, remote=remote, command="update"
)
+14 -12
View File
@@ -7,9 +7,6 @@ from funcy import cached_property
from dvc.progress import Tqdm
from dvc.utils import colorize
NEWLINE = "\n"
SEP = " "
class Formatter:
def __init__(self, theme: Dict = None, defaults: Dict = None) -> None:
@@ -30,14 +27,17 @@ class Console:
formatter: Formatter = None,
output: TextIO = None,
error: TextIO = None,
disable: bool = False,
enable: bool = False,
) -> None:
self._input: TextIO = sys.stdin
self._output: TextIO = output or sys.stdout
self._error: TextIO = error or sys.stderr
self.formatter: Formatter = formatter or Formatter()
self.disabled: bool = disable
self._enabled: bool = enable
def enable(self):
self._enabled = True
def success(self, message: str) -> None:
self.write(message, style="success")
@@ -52,8 +52,8 @@ class Console:
self,
*objects: Any,
style: str = None,
sep: str = SEP,
end: str = NEWLINE,
sep: str = None,
end: str = None,
flush: bool = False,
) -> None:
return self.write(
@@ -69,12 +69,12 @@ class Console:
self,
*objects: Any,
style: str = None,
sep: str = SEP,
end: str = NEWLINE,
sep: str = None,
end: str = None,
file: TextIO = None,
flush: bool = False,
) -> None:
if self.disabled:
if not self._enabled:
return
file = file or self._output
@@ -152,9 +152,11 @@ class Console:
self.write(ret)
if __name__ == "__main__":
ui = Console()
ui = Console()
if __name__ == "__main__":
ui.enable()
ui.write("No default remote set")
ui.success("Everything is up to date.")
ui.warn("Run queued experiments will be removed.")
+1 -1
View File
@@ -6,7 +6,7 @@
import os
import subprocess
_BASE_VERSION = "2.0.6"
_BASE_VERSION = "2.0.8"
def _generate_version(base_version):
+1 -1
View File
@@ -1,2 +1,2 @@
PyInstaller==3.5
PyInstaller==4.2
numpy==1.19.3
+49
View File
@@ -0,0 +1,49 @@
import argparse
import importlib
import pathlib
import textwrap
from subprocess import STDOUT, check_call
path = pathlib.Path(__file__).parent.absolute()
dvc = path.parent / "dvc"
pyinstaller = path / "pyinstaller"
innosetup = path / "innosetup"
fpm = path / "fpm"
parser = argparse.ArgumentParser()
parser.add_argument(
"pkg", choices=["deb", "rpm", "osxpkg", "exe"], help="package type"
)
args = parser.parse_args()
(dvc / "utils" / "build.py").write_text(f'PKG = "{args.pkg}"')
# Autogenerate version, similar to what we do in setup.py
spec = importlib.util.spec_from_file_location(
"dvc.version", dvc / "version.py"
)
dvc_version = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dvc_version)
version = dvc_version.__version__
(dvc / "version.py").write_text(
textwrap.dedent(
f"""\
# AUTOGENERATED at build time by scripts/build.py
__version__ = "{version}"
"""
)
)
check_call(
["python", "build.py"], cwd=pyinstaller, stderr=STDOUT,
)
if args.pkg == "exe":
check_call(
["python", "build.py"], cwd=innosetup, stderr=STDOUT,
)
else:
check_call(
["python", "build.py", args.pkg], cwd=fpm, stderr=STDOUT,
)
-139
View File
@@ -1,139 +0,0 @@
#!/bin/bash
set -e
set -x
FPM_FLAGS='--description "Data Version Control - datasets, models, and experiments versioning for ML or data science projects"'
FPM_FLAGS+=' -n dvc -s dir -f'
FPM_FLAGS+=' --url https://dvc.org'
FPM_FLAGS+=' --license "Apache License 2.0"'
if [[ "$(uname)" == 'Linux' ]]; then
INSTALL_DIR=usr
FPM_FLAGS+=' --depends "git >= 1.7.0"' # needed for gitpython
else
INSTALL_DIR=usr/local
FPM_FLAGS+=' --osxpkg-identifier-prefix com.iterative'
FPM_FLAGS+=' --after-install scripts/fpm/after-install.sh'
FPM_FLAGS+=' --after-remove scripts/fpm/after-remove.sh'
fi
BUILD_DIR=build
BIN_DIR=$BUILD_DIR/$INSTALL_DIR/bin
LIB_DIR=$BUILD_DIR/$INSTALL_DIR/lib
FPM_PACKAGE_DIRS="usr"
ZSH_CMPLT_DIR=usr/share/zsh/site-functions/_dvc
if [[ "$(uname)" == 'Linux' ]]; then
BASH_CMPLT_DIR=etc/bash_completion.d
FPM_PACKAGE_DIRS="$FPM_PACKAGE_DIRS etc"
else
BASH_CMPLT_DIR=usr/local/etc/bash_completion.d
fi
print_error() {
echo -e "\e[31m$1\e[0m" >&2
}
if [ ! -d "dvc" ]; then
print_error "Please run this script from repository root"
exit 1
fi
trap 'print_error "FAIL"; exit 1' ERR
print_info() {
echo -e "\e[32m$1\e[0m"
}
command_exists() {
command -v $1 >/dev/null 2>&1
}
fpm_build() {
print_info "Building $1..."
VERSION=$(python -c "import dvc; from dvc import __version__; print(str(__version__))")
eval "fpm -t $1 $FPM_FLAGS -v $VERSION -C $BUILD_DIR $FPM_PACKAGE_DIRS"
}
cleanup() {
print_info "Cleaning up..."
rm -rf build
}
install_dependencies() {
print_info "Installing fpm..."
if command_exists dnf; then
sudo dnf install ruby-devel gcc make rpm-build
elif command_exists yum; then
sudo yum install ruby-devel gcc make rpm-build
elif command_exists apt-get; then
sudo apt-get update -y
sudo apt-get install ruby-dev build-essential rpm python-pip python-dev
elif command_exists brew; then
brew install ruby
else
echo "Unable to install fpm dependencies" && exit 1
fi
gem install --no-document fpm
# https://github.com/iterative/dvc/issues/2970
gem uninstall -i /Users/travis/.rvm/gems/ruby-2.4.3@global rubygems-bundler
print_info "Installing requirements..."
pip install .[all]
pip install -r scripts/build-requirements.txt
}
build_dvc() {
print_info "Building dvc binary..."
pyinstaller \
--additional-hooks-dir $(pwd)/scripts/hooks dvc/__main__.py \
--name dvc \
--distpath $LIB_DIR \
--specpath $BUILD_DIR
$LIB_DIR/dvc/dvc --help
# NOTE: in osxpkg fpm replaces symlinks with actual file that it
# points to, so we need to use after-install hook. See FPM_FLAGS
# above.
if [[ "$(uname)" == 'Linux' ]]; then
mkdir -p $BIN_DIR
pushd $BIN_DIR
ln -s ../lib/dvc/dvc dvc
popd
$BIN_DIR/dvc --help
fi
# NOTE: temporarily not adding scripts to mac package. See [1]
# [1] https://github.com/iterative/dvc/issues/2585
if [[ "$(uname)" == 'Linux' ]]; then
mkdir -p $BUILD_DIR/$BASH_CMPLT_DIR
$LIB_DIR/dvc/dvc completion -s bash > $BUILD_DIR/$BASH_CMPLT_DIR/dvc
mkdir -p $BUILD_DIR/$ZSH_CMPLT_DIR
$LIB_DIR/dvc/dvc completion -s zsh > $BUILD_DIR/$ZSH_CMPLT_DIR/_dvc
fi
}
build() {
cleanup
echo "PKG = \"$1\"" >dvc/utils/build.py
build_dvc
fpm_build $1
}
install_dependencies
if [[ "$(uname)" == 'Linux' ]]; then
build rpm
build deb
else
build osxpkg
fi
cleanup
print_info "Successfully built dvc packages"
-48
View File
@@ -1,48 +0,0 @@
@echo Off
echo ====== Starting to build dvc installer for Windows... ======
if not exist dvc\NUL (echo Error: Please run this script from repository root && goto :error)
rmdir /Q /S build
rmdir /Q /S dist
del /Q /S dvc.spec
del /Q /S "dvc-*.exe"
where pip
if %errorlevel% neq 0 (echo Error: pip not found && goto :error)
where choco
if %errorlevel% neq 0 (echo Error: choco not found && goto :error)
choco install InnoSetup
call refreshenv
where iscc
if %errorlevel% neq 0 (echo Error: Couldn't find Inno Setup compiler. && goto :error)
echo ====== Installing requirements... ======
echo PKG = "exe" > dvc\utils\build.py
call pip install .[all] || goto :error
call pip install -r scripts\build-requirements.txt || goto :error
call dvc pull || goto :error
echo ====== Building dvc binary... ======
call pyinstaller --additional-hooks-dir scripts\hooks dvc/__main__.py --name dvc --specpath build
echo ====== Testing dvc binary... ======
call dist\dvc\dvc.exe version || goto :error
echo ====== Copying additional files... ======
copy scripts\innosetup\addSymLinkPermissions.ps1 dist\ || goto :error
echo ====== Building dvc installer... ======
set PYTHONPATH=%cd%
call python scripts\innosetup\config_gen.py || goto :error
call iscc scripts\innosetup\setup.iss || goto :error
echo ====== DONE ======
goto :EOF
:error
echo ====== FAIL ======
exit /b 1
+4
View File
@@ -0,0 +1,4 @@
/build
/*.deb
/*.rpm
/*.pkg
+96
View File
@@ -0,0 +1,96 @@
import argparse
import os
import pathlib
import shutil
from subprocess import STDOUT, check_call, check_output
path = pathlib.Path(__file__).parent.absolute()
dvc = path.parent.parent / "dvc"
pyinstaller = path.parent / "pyinstaller"
build = path / "build"
install = build / "usr"
parser = argparse.ArgumentParser()
parser.add_argument(
"pkg", choices=["deb", "rpm", "osxpkg"], help="package type"
)
args = parser.parse_args()
flags = [
"--description",
'"Data Version Control | Git for Data & Models"',
"-n",
"dvc",
"-s",
"dir",
"-f",
"--license",
'"Apache License 2.0"',
]
if args.pkg == "osxpkg":
install /= "local"
bash_dir = install / "etc" / "bash_completion.d"
dirs = ["usr"]
flags.extend(
[
"--osxpkg-identifier-prefix",
"com.iterative",
"--after-install",
path / "after-install.sh",
"--after-remove",
path / "after-remove.sh",
]
)
else:
bash_dir = build / "etc" / "bash_completion.d"
dirs = ["usr", "etc"]
flags.extend(["--depends", "git >= 1.7.0"]) # needed for gitpython
try:
shutil.rmtree(build)
except FileNotFoundError:
pass
lib = install / "lib"
lib.mkdir(parents=True)
shutil.copytree(pyinstaller / "dist" / "dvc", lib / "dvc")
if args.pkg != "osxpkg":
# NOTE: in osxpkg fpm replaces symlinks with actual file that it
# points to, so we need to use after-install hook.
(install / "bin").mkdir()
os.symlink("../lib/dvc/dvc", install / "bin" / "dvc")
bash_dir.mkdir(parents=True)
bash_completion = check_output(
[lib / "dvc" / "dvc", "completion", "-s", "bash"], text=True
)
(bash_dir / "dvc").write_text(bash_completion)
zsh_dir = build / "usr" / "share" / "zsh" / "site-funtions"
zsh_dir.mkdir(parents=True)
zsh_completion = check_output(
[lib / "dvc" / "dvc", "completion", "-s", "zsh"], text=True
)
(zsh_dir / "_dvc").write_text(zsh_completion)
version = check_output([lib / "dvc" / "dvc", "--version"], text=True).strip()
check_call(
[
"fpm",
"--verbose",
"-t",
args.pkg,
*flags,
"-v",
version,
"-C",
build,
*dirs,
],
cwd=path,
stderr=STDOUT,
)
@@ -1,8 +0,0 @@
"""Temporary fix for https://github.com/iterative/dvc/issues/5618."""
from PyInstaller.utils.hooks import ( # pylint:disable=import-error
collect_data_files,
copy_metadata,
)
datas = collect_data_files("googleapiclient.discovery")
datas += copy_metadata("google_api_python_client")
+5 -3
View File
@@ -1,4 +1,6 @@
dvc.ico
dvc_left.bmp
dvc_up.bmp
/dvc.ico
/dvc_left.bmp
/dvc_up.bmp
/config.ini
/build
/dvc*.exe
+42
View File
@@ -0,0 +1,42 @@
import argparse
import configparser
import os
import pathlib
import shutil
from subprocess import STDOUT, check_call, check_output
path = pathlib.Path(__file__).parent.absolute()
config = path / "config.ini"
dvc = path.parent.parent / "dvc"
pyinstaller = path.parent / "pyinstaller"
build = path / "build"
install = build / "usr"
parser = argparse.ArgumentParser()
args = parser.parse_args()
try:
shutil.rmtree(build)
except FileNotFoundError:
pass
build.mkdir()
shutil.copytree(pyinstaller / "dist" / "dvc", build / "dvc")
shutil.copy(path / "addSymLinkPermissions.ps1", build)
version = check_output(
[os.fspath(build / "dvc" / "dvc"), "--version"], text=True
).strip()
cfg = configparser.ConfigParser()
cfg.add_section("Version")
cfg.set("Version", "version", version)
with (path / "config.ini").open("w") as fobj:
cfg.write(fobj)
check_call(
["iscc", "setup.iss"], cwd=path, stderr=STDOUT,
)
-11
View File
@@ -1,11 +0,0 @@
# This script generates config.ini for setup.iss script
import configparser
from dvc import __version__
config = configparser.ConfigParser()
config.add_section("Version")
config.set("Version", "Version", __version__)
with open("scripts/innosetup/config.ini", "w") as f:
config.write(f)
+4 -4
View File
@@ -1,8 +1,8 @@
#define MyAppName "DVC (Data Version Control)"
#define MyAppVersion ReadIni(".\scripts\innosetup\config.ini", "Version", "version", "unknown")
#define MyAppVersion ReadIni(SourcePath + "\config.ini", "Version", "version", "unknown")
#define MyAppPublisher "Dmitry Petrov"
#define MyAppURL "https://dataversioncontrol.com/"
#define MyAppDir "..\..\dist\dvc"
#define MyAppURL "https://dvc.org"
#define MyAppDir SourcePath + "\build\dvc"
[Setup]
AppId={{8258CE8A-110E-4E0D-AE60-FEE00B15F041}
@@ -19,7 +19,7 @@ LicenseFile=..\..\LICENSE
OutputBaseFilename=dvc-{#MyAppVersion}
Compression=lzma2/max
SolidCompression=yes
OutputDir=..\..\
OutputDir=.
ChangesEnvironment=yes
SetupIconFile=dvc.ico
WizardSmallImageFile=dvc_up.bmp
+4
View File
@@ -0,0 +1,4 @@
/build
/dist
/dvc.spec
/dvc
+22
View File
@@ -0,0 +1,22 @@
import os
import pathlib
from subprocess import STDOUT, check_call
path = pathlib.Path(__file__).parent.absolute()
hooks = path / "hooks"
dvc = path.parent.parent / "dvc"
entry = dvc / "__main__.py"
check_call(
[
"pyinstaller",
"--additional-hooks-dir",
os.fspath(hooks),
"--name",
"dvc",
"-y",
os.fspath(entry),
],
cwd=path,
stderr=STDOUT,
)
+1 -1
View File
@@ -145,7 +145,7 @@ tests_requirements = [
"rangehttpserver==1.2.0",
"beautifulsoup4==4.4.0",
"pylint==2.7.2",
"pylint-pytest==0.3.0",
"pylint-pytest==1.0.3",
"pylint-plugin-utils==0.6",
"wget",
"filelock",
-130
View File
@@ -1,7 +1,3 @@
import os
import pytest
from dvc.utils import relpath
from dvc.utils.serialize import dump_yaml
@@ -18,132 +14,6 @@ def test_diff_no_changes(tmp_dir, scm, dvc):
assert dvc.params.diff() == {}
@pytest.mark.parametrize("commit_last", [True, False])
def test_diff_add_similar_files(tmp_dir, scm, dvc, commit_last):
if commit_last:
last_commit_msg = "commit #2"
a_rev = "HEAD~1"
else:
last_commit_msg = None
a_rev = "HEAD"
tmp_dir.dvc_gen(
{"dir": {"file": "text1", "subdir": {"file2": "text2"}}},
commit="commit #1",
)
tmp_dir.dvc_gen(
{"dir2": {"file": "text1", "subdir": {"file2": "text2"}}},
commit=last_commit_msg,
)
assert dvc.diff(a_rev) == {
"added": [
{
"path": os.path.join("dir2", ""),
"hash": "cb58ee07cb01044db229e4d6121a0dfc.dir",
},
{
"path": os.path.join("dir2", "file"),
"hash": "cef7ccd89dacf1ced6f5ec91d759953f",
},
{
"path": os.path.join("dir2", "subdir", "file2"),
"hash": "fe6123a759017e4a2af4a2d19961ed71",
},
],
"deleted": [],
"modified": [],
"renamed": [],
"not in cache": [],
}
@pytest.mark.parametrize("commit_last", [True, False])
def test_diff_rename_folder(tmp_dir, scm, dvc, commit_last):
if commit_last:
last_commit_msg = "commit #2"
a_rev = "HEAD~1"
else:
last_commit_msg = None
a_rev = "HEAD"
tmp_dir.dvc_gen(
{"dir": {"file": "text1", "subdir": {"file2": "text2"}}},
commit="commit #1",
)
(tmp_dir / "dir").replace(tmp_dir / "dir2")
tmp_dir.dvc_add("dir2", commit=last_commit_msg)
assert dvc.diff(a_rev) == {
"added": [],
"deleted": [],
"modified": [],
"renamed": [
{
"path": {
"old": os.path.join("dir", ""),
"new": os.path.join("dir2", ""),
},
"hash": "cb58ee07cb01044db229e4d6121a0dfc.dir",
},
{
"path": {
"old": os.path.join("dir", "file"),
"new": os.path.join("dir2", "file"),
},
"hash": "cef7ccd89dacf1ced6f5ec91d759953f",
},
{
"path": {
"old": os.path.join("dir", "subdir", "file2"),
"new": os.path.join("dir2", "subdir", "file2"),
},
"hash": "fe6123a759017e4a2af4a2d19961ed71",
},
],
"not in cache": [],
}
@pytest.mark.parametrize("commit_last", [True, False])
def test_diff_rename_file(tmp_dir, scm, dvc, commit_last):
if commit_last:
last_commit_msg = "commit #2"
a_rev = "HEAD~1"
else:
last_commit_msg = None
a_rev = "HEAD"
paths = tmp_dir.gen(
{"dir": {"file": "text1", "subdir": {"file2": "text2"}}}
)
tmp_dir.dvc_add(paths, commit="commit #1")
(tmp_dir / "dir" / "file").replace(tmp_dir / "dir" / "subdir" / "file3")
tmp_dir.dvc_add(paths, commit=last_commit_msg)
assert dvc.diff(a_rev) == {
"added": [],
"deleted": [],
"modified": [
{
"path": os.path.join("dir", ""),
"hash": {
"old": "cb58ee07cb01044db229e4d6121a0dfc.dir",
"new": "a4ac9c339aacc60b6a3152e362c319c8.dir",
},
}
],
"renamed": [
{
"path": {
"old": os.path.join("dir", "file"),
"new": os.path.join("dir", "subdir", "file3"),
},
"hash": "cef7ccd89dacf1ced6f5ec91d759953f",
}
],
"not in cache": [],
}
def test_diff(tmp_dir, scm, dvc):
tmp_dir.gen("params.yaml", "foo: bar")
dvc.run(cmd="echo params.yaml", params=["foo"], single_stage=True)
+1 -1
View File
@@ -708,7 +708,7 @@ def test_show_from_subdir(tmp_dir, dvc, caplog):
with subdir.chdir(), caplog.at_level(logging.INFO, "dvc"):
assert main(["plots", "show", "metric.json"]) == 0
assert f"file://{str(subdir)}" in caplog.text
assert subdir.as_uri() in caplog.text
assert (subdir / "plots.html").exists()
+172
View File
@@ -423,3 +423,175 @@ def test_targets_single_file_in_dir_with_file(tmp_dir, scm, dvc):
"not in cache": [],
"renamed": [],
}
@pytest.mark.parametrize("commit_last", [True, False])
def test_diff_add_similar_files(tmp_dir, scm, dvc, commit_last):
if commit_last:
last_commit_msg = "commit #2"
a_rev = "HEAD~1"
else:
last_commit_msg = None
a_rev = "HEAD"
tmp_dir.dvc_gen(
{"dir": {"file": "text1", "subdir": {"file2": "text2"}}},
commit="commit #1",
)
tmp_dir.dvc_gen(
{"dir2": {"file": "text1", "subdir": {"file2": "text2"}}},
commit=last_commit_msg,
)
assert dvc.diff(a_rev) == {
"added": [
{
"path": os.path.join("dir2", ""),
"hash": "cb58ee07cb01044db229e4d6121a0dfc.dir",
},
{
"path": os.path.join("dir2", "file"),
"hash": "cef7ccd89dacf1ced6f5ec91d759953f",
},
{
"path": os.path.join("dir2", "subdir", "file2"),
"hash": "fe6123a759017e4a2af4a2d19961ed71",
},
],
"deleted": [],
"modified": [],
"renamed": [],
"not in cache": [],
}
@pytest.mark.parametrize("commit_last", [True, False])
def test_diff_rename_folder(tmp_dir, scm, dvc, commit_last):
if commit_last:
last_commit_msg = "commit #2"
a_rev = "HEAD~1"
else:
last_commit_msg = None
a_rev = "HEAD"
tmp_dir.dvc_gen(
{"dir": {"file": "text1", "subdir": {"file2": "text2"}}},
commit="commit #1",
)
(tmp_dir / "dir").replace(tmp_dir / "dir2")
tmp_dir.dvc_add("dir2", commit=last_commit_msg)
assert dvc.diff(a_rev) == {
"added": [],
"deleted": [],
"modified": [],
"renamed": [
{
"path": {
"old": os.path.join("dir", ""),
"new": os.path.join("dir2", ""),
},
"hash": "cb58ee07cb01044db229e4d6121a0dfc.dir",
},
{
"path": {
"old": os.path.join("dir", "file"),
"new": os.path.join("dir2", "file"),
},
"hash": "cef7ccd89dacf1ced6f5ec91d759953f",
},
{
"path": {
"old": os.path.join("dir", "subdir", "file2"),
"new": os.path.join("dir2", "subdir", "file2"),
},
"hash": "fe6123a759017e4a2af4a2d19961ed71",
},
],
"not in cache": [],
}
@pytest.mark.parametrize("commit_last", [True, False])
def test_diff_rename_file(tmp_dir, scm, dvc, commit_last):
if commit_last:
last_commit_msg = "commit #2"
a_rev = "HEAD~1"
else:
last_commit_msg = None
a_rev = "HEAD"
paths = tmp_dir.gen(
{"dir": {"file": "text1", "subdir": {"file2": "text2"}}}
)
tmp_dir.dvc_add(paths, commit="commit #1")
(tmp_dir / "dir" / "file").replace(tmp_dir / "dir" / "subdir" / "file3")
tmp_dir.dvc_add(paths, commit=last_commit_msg)
assert dvc.diff(a_rev) == {
"added": [],
"deleted": [],
"modified": [
{
"path": os.path.join("dir", ""),
"hash": {
"old": "cb58ee07cb01044db229e4d6121a0dfc.dir",
"new": "a4ac9c339aacc60b6a3152e362c319c8.dir",
},
}
],
"renamed": [
{
"path": {
"old": os.path.join("dir", "file"),
"new": os.path.join("dir", "subdir", "file3"),
},
"hash": "cef7ccd89dacf1ced6f5ec91d759953f",
}
],
"not in cache": [],
}
def test_rename_multiple_files_same_hashes(tmp_dir, scm, dvc):
"""Test diff by renaming >=2 instances of file with same hashes.
DVC should be able to detect that they are renames, and should not include
them in either of the `added` or the `deleted` section.
"""
tmp_dir.dvc_gen(
{"dir": {"foo": "foo", "subdir": {"foo": "foo"}}}, commit="commit #1",
)
remove(tmp_dir / "dir")
# changing foo and subdir/foo to bar and subdir/bar respectively
tmp_dir.dvc_gen(
{"dir": {"bar": "foo", "subdir": {"bar": "foo"}}}, commit="commit #2"
)
assert dvc.diff("HEAD~") == {
"added": [],
"deleted": [],
"modified": [
{
"hash": {
"new": "31b36b3ea5f4485e27f10578c47183b0.dir",
"old": "c7684c8b3b0d28cf80d5305e2d856bfc.dir",
},
"path": os.path.join("dir", ""),
}
],
"not in cache": [],
"renamed": [
{
"hash": "acbd18db4cc2f85cedef654fccc4a4d8",
"path": {
"new": os.path.join("dir", "bar"),
"old": os.path.join("dir", "foo"),
},
},
{
"hash": "acbd18db4cc2f85cedef654fccc4a4d8",
"path": {
"new": os.path.join("dir", "subdir", "bar"),
"old": os.path.join("dir", "subdir", "foo"),
},
},
],
}
+20
View File
@@ -349,3 +349,23 @@ def test_fs_ls_with_etag(dvc, cloud):
fs.info(path_info.replace(path=details["name"]))["etag"]
== details["etag"]
)
@pytest.mark.parametrize(
"cloud", [pytest.lazy_fixture("azure"), pytest.lazy_fixture("gs")]
)
def test_fs_fsspec_path_management(dvc, cloud):
cloud.gen({"foo": "foo", "data": {"bar": "bar", "baz": {"foo": "foo"}}})
fs = get_cloud_fs(dvc, **cloud.config)
root = cloud.parents[len(cloud.parents) - 1]
bucket_details = fs.info(root)
# special conditions: name always points to the bucket name
assert bucket_details["name"] == root.bucket
assert bucket_details["type"] == "directory"
data = cloud / "data"
data_details = fs.info(data)
assert data_details["name"].rstrip("/") == data.path
assert data_details["type"] == "directory"
+33 -6
View File
@@ -1,4 +1,8 @@
import logging
import os
import posixpath
import pytest
from dvc.cli import parse_args
from dvc.command.plots import CmdPlotsDiff, CmdPlotsShow
@@ -116,12 +120,11 @@ def test_plots_diff_open(tmp_dir, dvc, mocker, caplog):
)
assert cmd.run() == 0
mocked_open.assert_called_once_with("plots.html")
expected_url = f"file://{tmp_dir / 'plots.html'}"
expected_url = posixpath.join(tmp_dir.as_uri(), "plots.html")
assert expected_url in caplog.text
mocked_open.assert_called_once_with(expected_url)
def test_plots_diff_open_failed(tmp_dir, dvc, mocker, caplog):
mocked_open = mocker.patch("webbrowser.open", return_value=False)
@@ -132,12 +135,36 @@ def test_plots_diff_open_failed(tmp_dir, dvc, mocker, caplog):
)
assert cmd.run() == 1
expected_url = f"file://{tmp_dir / 'plots.html'}"
mocked_open.assert_called_once_with(expected_url)
mocked_open.assert_called_once_with("plots.html")
error_message = "Failed to open. Please try opening it manually."
expected_url = posixpath.join(tmp_dir.as_uri(), "plots.html")
assert caplog.record_tuples == [
("dvc.command.plots", logging.INFO, expected_url),
("dvc.command.plots", logging.ERROR, error_message),
]
@pytest.mark.parametrize(
"output, expected_url_path",
[
("plots file with spaces.html", "plots%20file%20with%20spaces.html"),
(os.path.join("dir", "..", "plots.html"), "plots.html"),
],
ids=["quote", "resolve"],
)
def test_plots_path_is_quoted_and_resolved_properly(
tmp_dir, dvc, mocker, caplog, output, expected_url_path
):
cli_args = parse_args(
["plots", "diff", "--targets", "datafile", "--out", output]
)
cmd = cli_args.func(cli_args)
mocker.patch(
"dvc.repo.plots.diff.diff", return_value={"datafile": "filledtemplate"}
)
assert cmd.run() == 0
expected_url = posixpath.join(tmp_dir.as_uri(), expected_url_path)
assert expected_url in caplog.text
+28
View File
@@ -248,3 +248,31 @@ def test_info_with_debug_loglevel_shows_no_datetime(caplog, dt):
logger.info("message")
assert "message" == formatter.format(caplog.records[0])
def test_add_existing_level(caplog, dt):
# Common pattern to configure logging level in external libraries
# eg:
# https://github.com/bokeh/bokeh/blob/04bb30fef2e72e64baaa8b2f330806d5bfdd3b11/
# bokeh/util/logconfig.py#L79-L85
TRACE2 = 4
logging.addLevelName(TRACE2, "TRACE2")
logging.TRACE2 = TRACE2
dvc.logger.addLoggingLevel("TRACE2", 2)
# DVC sets all expected entrypoints, but doesn't override the level
assert logging.TRACE2 == 4
assert hasattr(logging, "trace2")
assert hasattr(logger, "trace2")
assert logging.getLevelName("TRACE2") == 4
# The TRACE2 logging level uses the original, higher logging level
with caplog.at_level(logging.TRACE2, logger="dvc"):
logger.trace2("TRACE2")
assert len(caplog.records) == 1
(record,) = caplog.records
assert record.levelno == 4
assert record.levelname == "TRACE2"
assert record.message == "TRACE2"