mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-10 15:28:34 +01:00
Merge pull request #1514 from efiop/master
RFC: second iteration of complying with pylint
This commit is contained in:
+10
-10
@@ -53,7 +53,7 @@ class Project(object):
|
||||
self.cloud = DataCloud(self, config=self.config.config)
|
||||
self.updater = Updater(self.dvc_dir)
|
||||
|
||||
self._files_to_git_add = []
|
||||
self.files_to_git_add = []
|
||||
|
||||
self._ignore()
|
||||
|
||||
@@ -84,14 +84,14 @@ class Project(object):
|
||||
return os.path.join(root_dir, Project.DVC_DIR)
|
||||
|
||||
def _remind_to_git_add(self):
|
||||
if not self._files_to_git_add:
|
||||
if not self.files_to_git_add:
|
||||
return
|
||||
|
||||
logger.info('\n'
|
||||
'To track the changes with git run:\n'
|
||||
'\n'
|
||||
'\tgit add {files}'
|
||||
.format(files=' '.join(self._files_to_git_add)))
|
||||
.format(files=' '.join(self.files_to_git_add)))
|
||||
|
||||
@staticmethod
|
||||
def init(root_dir=os.curdir, no_scm=False, force=False):
|
||||
@@ -143,8 +143,8 @@ class Project(object):
|
||||
|
||||
scm.add([config.config_file])
|
||||
|
||||
if scm.ignore_file():
|
||||
scm.add([os.path.join(dvc_dir, scm.ignore_file())])
|
||||
if scm.ignore_file:
|
||||
scm.add([os.path.join(dvc_dir, scm.ignore_file)])
|
||||
logger.info('\nYou can now commit the changes to git.\n')
|
||||
|
||||
proj._welcome_message()
|
||||
@@ -197,7 +197,7 @@ class Project(object):
|
||||
path = os.path.join(root, f)
|
||||
if Stage.is_stage_file(path):
|
||||
continue
|
||||
if os.path.basename(path) == self.scm.ignore_file():
|
||||
if os.path.basename(path) == self.scm.ignore_file:
|
||||
continue
|
||||
if self.scm.is_tracked(path):
|
||||
continue
|
||||
@@ -206,7 +206,7 @@ class Project(object):
|
||||
fnames = [fname]
|
||||
|
||||
stages = []
|
||||
self._files_to_git_add = []
|
||||
self.files_to_git_add = []
|
||||
with self.state:
|
||||
for f in fnames:
|
||||
stage = Stage.create(project=self,
|
||||
@@ -386,7 +386,7 @@ class Project(object):
|
||||
|
||||
self._check_dag(self.stages() + [stage])
|
||||
|
||||
self._files_to_git_add = []
|
||||
self.files_to_git_add = []
|
||||
with self.state:
|
||||
if not no_exec:
|
||||
stage.run()
|
||||
@@ -410,7 +410,7 @@ class Project(object):
|
||||
|
||||
self._check_dag(self.stages() + [stage])
|
||||
|
||||
self._files_to_git_add = []
|
||||
self.files_to_git_add = []
|
||||
with self.state:
|
||||
stage.run()
|
||||
|
||||
@@ -474,7 +474,7 @@ class Project(object):
|
||||
else:
|
||||
targets.append(target)
|
||||
|
||||
self._files_to_git_add = []
|
||||
self.files_to_git_add = []
|
||||
|
||||
ret = []
|
||||
with self.state:
|
||||
|
||||
+22
-4
@@ -1,16 +1,18 @@
|
||||
"""Manages user prompts."""
|
||||
|
||||
import sys
|
||||
from getpass import getpass
|
||||
|
||||
try:
|
||||
# NOTE: in Python3 raw_input() was renamed to input()
|
||||
input = raw_input
|
||||
input = raw_input # pylint: disable=redefined-builtin, invalid-name
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
|
||||
def ask(prompt, limited_to=None):
|
||||
def _ask(prompt, limited_to=None):
|
||||
if not sys.stdout.isatty():
|
||||
return
|
||||
return None
|
||||
|
||||
while True:
|
||||
answer = input(prompt + ' ').lower()
|
||||
@@ -26,11 +28,27 @@ def ask(prompt, limited_to=None):
|
||||
|
||||
|
||||
def confirm(statement):
|
||||
"""Ask the user for confirmation about the specified statement.
|
||||
|
||||
Args:
|
||||
statement (str): statement to ask the user confirmation about.
|
||||
|
||||
Returns:
|
||||
bool: whether or not specified statement was confirmed.
|
||||
"""
|
||||
prompt = '{statement} [y/n]'.format(statement=statement)
|
||||
answer = ask(prompt, limited_to=['yes', 'no', 'y', 'n'])
|
||||
answer = _ask(prompt, limited_to=['yes', 'no', 'y', 'n'])
|
||||
return answer and answer.startswith('y')
|
||||
|
||||
|
||||
def password(statement):
|
||||
"""Ask the user for a password.
|
||||
|
||||
Args:
|
||||
statement (str): string to prompt the user with.
|
||||
|
||||
Returns:
|
||||
str: password entered by the user.
|
||||
"""
|
||||
prompt = '{statement}: '.format(statement=statement)
|
||||
return getpass(prompt)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Manages source control systems(e.g. Git)."""
|
||||
|
||||
from dvc.scm.base import Base
|
||||
from dvc.scm.git import Git
|
||||
|
||||
|
||||
def SCM(root_dir, project=None): # pylint: disable=invalid-name
|
||||
"""Returns SCM instance that corresponds to a project at the specified
|
||||
path.
|
||||
|
||||
Args:
|
||||
root_dir (str): path to a root directory of the project.
|
||||
project (dvc.project.Project): dvc project instance that root_dir
|
||||
belongs to.
|
||||
|
||||
Returns:
|
||||
dvc.scm.base.Base: SCM instance.
|
||||
"""
|
||||
if Git.is_repo(root_dir) or Git.is_submodule(root_dir):
|
||||
return Git(root_dir, project=project)
|
||||
|
||||
return Base(root_dir, project=project)
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
"""Manages source control systems(e.g. Git) in dvc."""
|
||||
|
||||
import os
|
||||
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class SCMError(DvcException):
|
||||
"""Base class for source control management errors."""
|
||||
|
||||
|
||||
class FileNotInRepoError(DvcException):
|
||||
"""Thrown when trying to find .gitignore for a file that is not in a scm
|
||||
repository.
|
||||
"""
|
||||
|
||||
|
||||
class Base(object):
|
||||
"""Base class for source control management driver implementations."""
|
||||
def __init__(self, root_dir=os.curdir, project=None):
|
||||
self.project = project
|
||||
self.root_dir = root_dir
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{directory}'".format(
|
||||
class_name=type(self).__name__,
|
||||
directory=self.dir
|
||||
)
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
"""Path to a directory with SCM specific information."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_repo(root_dir): # pylint: disable=unused-argument
|
||||
"""Returns whether or not root_dir is a valid SCM repository."""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_submodule(root_dir): # pylint: disable=unused-argument
|
||||
"""Returns whether or not root_dir is a valid SCM repository
|
||||
submodule.
|
||||
"""
|
||||
return True
|
||||
|
||||
def ignore(self, path): # pylint: disable=unused-argument
|
||||
"""Makes SCM ignore a specified path."""
|
||||
|
||||
def ignore_remove(self, path): # pylint: disable=unused-argument
|
||||
"""Makes SCM stop ignoring a specified path."""
|
||||
|
||||
@property
|
||||
def ignore_file(self):
|
||||
"""Filename for a file that contains ignored paths for this SCM."""
|
||||
|
||||
def ignore_list(self, p_list):
|
||||
"""Makes SCM ignore all paths specified in a list."""
|
||||
return [self.ignore(path) for path in p_list]
|
||||
|
||||
def add(self, paths):
|
||||
"""Makes SCM start tracking every path from a specified list of paths.
|
||||
"""
|
||||
|
||||
def commit(self, msg):
|
||||
"""Makes SCM create a commit."""
|
||||
|
||||
def checkout(self, branch, create_new=False):
|
||||
"""Makes SCM checkout a branch."""
|
||||
|
||||
def branch(self, branch):
|
||||
"""Makes SCM create a branch with a specified name."""
|
||||
|
||||
def tag(self, tag):
|
||||
"""Makes SCM create a tag with a specified name."""
|
||||
|
||||
def brancher(self,
|
||||
branches=None,
|
||||
all_branches=False,
|
||||
tags=None,
|
||||
all_tags=False):
|
||||
"""Generator that iterates over specified revisions.
|
||||
|
||||
Args:
|
||||
branches (list): a list of branches to iterate over.
|
||||
all_branches (bool): iterate over all available branches.
|
||||
tags (list): a list of tags to iterate over.
|
||||
all_tags (bool): iterate over all available tags.
|
||||
|
||||
Yields:
|
||||
str: the current revision.
|
||||
"""
|
||||
if not branches and not all_branches \
|
||||
and not tags and not all_tags:
|
||||
yield ''
|
||||
return
|
||||
|
||||
saved = self.active_branch()
|
||||
revs = []
|
||||
|
||||
if all_branches:
|
||||
branches = self.list_branches()
|
||||
|
||||
if all_tags:
|
||||
tags = self.list_tags()
|
||||
|
||||
if branches is None:
|
||||
revs.extend([saved])
|
||||
else:
|
||||
revs.extend(branches)
|
||||
|
||||
if tags is not None:
|
||||
revs.extend(tags)
|
||||
|
||||
for rev in revs:
|
||||
self.checkout(rev)
|
||||
yield rev
|
||||
|
||||
self.checkout(saved)
|
||||
|
||||
def untracked_files(self): # pylint: disable=no-self-use
|
||||
"""Returns a list of untracked files."""
|
||||
return []
|
||||
|
||||
def is_tracked(self, path): # pylint: disable=no-self-use, unused-argument
|
||||
"""Returns whether or not a specified path is tracked."""
|
||||
return False
|
||||
|
||||
def active_branch(self): # pylint: disable=no-self-use
|
||||
"""Returns current branch in the project."""
|
||||
return ''
|
||||
|
||||
def list_branches(self): # pylint: disable=no-self-use
|
||||
"""Returns a list of available branches in the project."""
|
||||
return []
|
||||
|
||||
def list_tags(self): # pylint: disable=no-self-use
|
||||
"""Returns a list of available tags in the project."""
|
||||
return []
|
||||
|
||||
def install(self):
|
||||
"""Adds dvc commands to SCM hooks for the project."""
|
||||
+23
-132
@@ -1,121 +1,18 @@
|
||||
"""Manages Git."""
|
||||
|
||||
# NOTE: need this one because python 2 would re-import this module
|
||||
# (called git.py) instead of importing gitpython's git module.
|
||||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
|
||||
import dvc.logger as logger
|
||||
from dvc.exceptions import DvcException
|
||||
from dvc.utils import fix_env
|
||||
|
||||
|
||||
class SCMError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class FileNotInRepoError(DvcException):
|
||||
pass
|
||||
|
||||
|
||||
class Base(object):
|
||||
def __init__(self, root_dir=os.curdir, project=None):
|
||||
self.project = project
|
||||
self.root_dir = root_dir
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_name}: '{directory}'".format(
|
||||
class_name=type(self).__name__,
|
||||
directory=self.dir
|
||||
)
|
||||
|
||||
@property
|
||||
def dir(self):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_repo(root_dir):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_submodule(root_dir):
|
||||
return True
|
||||
|
||||
def ignore(self, path):
|
||||
pass
|
||||
|
||||
def ignore_remove(self, path):
|
||||
pass
|
||||
|
||||
def ignore_file(self):
|
||||
pass
|
||||
|
||||
def ignore_list(self, p_list):
|
||||
return [self.ignore(path) for path in p_list]
|
||||
|
||||
def add(self, paths):
|
||||
pass
|
||||
|
||||
def commit(self, msg):
|
||||
pass
|
||||
|
||||
def checkout(self, branch):
|
||||
pass
|
||||
|
||||
def branch(self, branch):
|
||||
pass
|
||||
|
||||
def tag(self, tag):
|
||||
pass
|
||||
|
||||
def brancher(self,
|
||||
branches=None,
|
||||
all_branches=False,
|
||||
tags=None,
|
||||
all_tags=False):
|
||||
if not branches and not all_branches \
|
||||
and not tags and not all_tags:
|
||||
yield ''
|
||||
return
|
||||
|
||||
saved = self.active_branch()
|
||||
revs = []
|
||||
|
||||
if all_branches:
|
||||
branches = self.list_branches()
|
||||
|
||||
if all_tags:
|
||||
tags = self.list_tags()
|
||||
|
||||
if branches is None:
|
||||
revs.extend([saved])
|
||||
else:
|
||||
revs.extend(branches)
|
||||
|
||||
if tags is not None:
|
||||
revs.extend(tags)
|
||||
|
||||
for rev in revs:
|
||||
self.checkout(rev)
|
||||
yield rev
|
||||
|
||||
self.checkout(saved)
|
||||
|
||||
def untracked_files(self):
|
||||
pass
|
||||
|
||||
def is_tracked(self, path):
|
||||
pass
|
||||
|
||||
def active_branch(self):
|
||||
pass
|
||||
|
||||
def list_branches(self):
|
||||
pass
|
||||
|
||||
def list_tags(self):
|
||||
pass
|
||||
|
||||
def install(self):
|
||||
pass
|
||||
from dvc.scm.base import Base, SCMError, FileNotInRepoError
|
||||
|
||||
|
||||
class Git(Base):
|
||||
"""Class for managing Git."""
|
||||
GITIGNORE = '.gitignore'
|
||||
GIT_DIR = '.git'
|
||||
|
||||
@@ -133,8 +30,8 @@ class Git(Base):
|
||||
# NOTE: fixing LD_LIBRARY_PATH for binary built by PyInstaller.
|
||||
# http://pyinstaller.readthedocs.io/en/stable/runtime-information.html
|
||||
env = fix_env(None)
|
||||
lp = env.get('LD_LIBRARY_PATH', None)
|
||||
self.repo.git.update_environment(LD_LIBRARY_PATH=lp)
|
||||
libpath = env.get('LD_LIBRARY_PATH', None)
|
||||
self.repo.git.update_environment(LD_LIBRARY_PATH=libpath)
|
||||
|
||||
@staticmethod
|
||||
def is_repo(root_dir):
|
||||
@@ -152,6 +49,7 @@ class Git(Base):
|
||||
def dir(self):
|
||||
return self.repo.git_dir
|
||||
|
||||
@property
|
||||
def ignore_file(self):
|
||||
return self.GITIGNORE
|
||||
|
||||
@@ -174,7 +72,7 @@ class Git(Base):
|
||||
ignore_list = open(gitignore, 'r').readlines()
|
||||
filtered = list(filter(lambda x: x.strip() == entry.strip(),
|
||||
ignore_list))
|
||||
if len(filtered) != 0:
|
||||
if filtered:
|
||||
return
|
||||
|
||||
msg = "Adding '{}' to '{}'.".format(os.path.relpath(path),
|
||||
@@ -182,14 +80,14 @@ class Git(Base):
|
||||
logger.info(msg)
|
||||
|
||||
content = entry
|
||||
if len(ignore_list) > 0:
|
||||
if ignore_list:
|
||||
content = '\n' + content
|
||||
|
||||
with open(gitignore, 'a') as fd:
|
||||
fd.write(content)
|
||||
with open(gitignore, 'a') as fobj:
|
||||
fobj.write(content)
|
||||
|
||||
if self.project is not None:
|
||||
self.project._files_to_git_add.append(os.path.relpath(gitignore))
|
||||
self.project.files_to_git_add.append(os.path.relpath(gitignore))
|
||||
|
||||
def ignore_remove(self, path):
|
||||
entry, gitignore = self._get_gitignore(path)
|
||||
@@ -197,16 +95,16 @@ class Git(Base):
|
||||
if not os.path.exists(gitignore):
|
||||
return
|
||||
|
||||
with open(gitignore, 'r') as fd:
|
||||
lines = fd.readlines()
|
||||
with open(gitignore, 'r') as fobj:
|
||||
lines = fobj.readlines()
|
||||
|
||||
filtered = list(filter(lambda x: x.strip() != entry.strip(), lines))
|
||||
|
||||
with open(gitignore, 'w') as fd:
|
||||
fd.writelines(filtered)
|
||||
with open(gitignore, 'w') as fobj:
|
||||
fobj.writelines(filtered)
|
||||
|
||||
if self.project is not None:
|
||||
self.project._files_to_git_add.append(os.path.relpath(gitignore))
|
||||
self.project.files_to_git_add.append(os.path.relpath(gitignore))
|
||||
|
||||
def add(self, paths):
|
||||
# NOTE: GitPython is not currently able to handle index version >= 3.
|
||||
@@ -263,17 +161,10 @@ class Git(Base):
|
||||
if os.path.isfile(hook):
|
||||
msg = "git hook '{}' already exists."
|
||||
raise SCMError(msg.format(os.path.relpath(hook)))
|
||||
with open(hook, 'w+') as fd:
|
||||
fd.write('#!/bin/sh\nexec dvc {}\n'.format(cmd))
|
||||
with open(hook, 'w+') as fobj:
|
||||
fobj.write('#!/bin/sh\nexec dvc {}\n'.format(cmd))
|
||||
os.chmod(hook, 0o777)
|
||||
|
||||
def install(self):
|
||||
self._install_hook('post-checkout', 'checkout')
|
||||
self._install_hook('pre-commit', 'status')
|
||||
|
||||
|
||||
def SCM(root_dir, no_scm=False, project=None):
|
||||
if Git.is_repo(root_dir) or Git.is_submodule(root_dir):
|
||||
return Git(root_dir, project=project)
|
||||
|
||||
return Base(root_dir, project=project)
|
||||
+1
-1
@@ -458,7 +458,7 @@ class Stage(object):
|
||||
with open(fname, 'w') as fd:
|
||||
yaml.safe_dump(self.dumpd(), fd, default_flow_style=False)
|
||||
|
||||
self.project._files_to_git_add.append(os.path.relpath(fname))
|
||||
self.project.files_to_git_add.append(os.path.relpath(fname))
|
||||
|
||||
def _compute_md5(self):
|
||||
from dvc.output.local import OutputLOCAL
|
||||
|
||||
+151
-89
@@ -1,3 +1,5 @@
|
||||
"""Manages state database used for checksum caching."""
|
||||
|
||||
import os
|
||||
import time
|
||||
import sqlite3
|
||||
@@ -10,11 +12,31 @@ from dvc.utils import file_md5, remove
|
||||
from dvc.exceptions import DvcException
|
||||
|
||||
|
||||
class StateDuplicateError(DvcException):
|
||||
pass
|
||||
class StateVersionTooNewError(DvcException):
|
||||
"""Thrown when dvc version is older than the state database version."""
|
||||
def __init__(self, dvc_version, expected, actual):
|
||||
super(StateVersionTooNewError, self).__init__(
|
||||
"you are using an old version '{dvc_version}' of dvc that is "
|
||||
"using state file version '{expected}' which is not compatible "
|
||||
"with the state file version '{actual}' that is used in this "
|
||||
"projet. Please upgrade right now!"
|
||||
.format(dvc_version=dvc_version,
|
||||
expected=expected,
|
||||
actual=actual))
|
||||
|
||||
|
||||
class State(object):
|
||||
class State(object): # pylint: disable=too-many-instance-attributes
|
||||
"""Class for the state database.
|
||||
|
||||
Args:
|
||||
project (dvc.project.Project): project instance that this state
|
||||
belongs to.
|
||||
config (configobj.ConfigObj): config for the state.
|
||||
|
||||
Raises:
|
||||
StateVersionTooNewError: thrown when dvc version is older than the
|
||||
state database version.
|
||||
"""
|
||||
VERSION = 3
|
||||
STATE_FILE = 'state'
|
||||
STATE_TABLE = 'state'
|
||||
@@ -47,11 +69,12 @@ class State(object):
|
||||
self.row_limit = 100
|
||||
self.row_cleanup_quota = 50
|
||||
|
||||
c = config.get(Config.SECTION_STATE, {})
|
||||
self.row_limit = c.get(Config.SECTION_STATE_ROW_LIMIT,
|
||||
self.STATE_ROW_LIMIT)
|
||||
self.row_cleanup_quota = c.get(Config.SECTION_STATE_ROW_CLEANUP_QUOTA,
|
||||
self.STATE_ROW_CLEANUP_QUOTA)
|
||||
state_config = config.get(Config.SECTION_STATE, {})
|
||||
self.row_limit = state_config.get(Config.SECTION_STATE_ROW_LIMIT,
|
||||
self.STATE_ROW_LIMIT)
|
||||
self.row_cleanup_quota = state_config.get(
|
||||
Config.SECTION_STATE_ROW_CLEANUP_QUOTA,
|
||||
self.STATE_ROW_CLEANUP_QUOTA)
|
||||
|
||||
if not self.dvc_dir:
|
||||
self.state_file = None
|
||||
@@ -65,23 +88,31 @@ class State(object):
|
||||
self.state_file + '-wal',
|
||||
]
|
||||
|
||||
self.db = None
|
||||
self.c = None
|
||||
self.database = None
|
||||
self.cursor = None
|
||||
self.inserts = 0
|
||||
|
||||
def __enter__(self):
|
||||
self.load()
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
def __exit__(self, typ, value, tbck):
|
||||
self.dump()
|
||||
|
||||
def _collect(self, path):
|
||||
if os.path.isdir(path):
|
||||
return self.project.cache.local.collect_dir_cache(path)
|
||||
else:
|
||||
return (file_md5(path)[0], None)
|
||||
return (file_md5(path)[0], None)
|
||||
|
||||
def changed(self, path, md5):
|
||||
"""Check if file/directory has the expected md5.
|
||||
|
||||
Args:
|
||||
path (str): path to the file/directory to check.
|
||||
md5 (str): expected md5.
|
||||
|
||||
Returns:
|
||||
bool: True if path has the expected md5, False otherwise.
|
||||
"""
|
||||
actual = self.update(path)
|
||||
|
||||
msg = "File '{}', md5 '{}', actual '{}'"
|
||||
@@ -94,32 +125,32 @@ class State(object):
|
||||
|
||||
def _execute(self, cmd):
|
||||
logger.debug(cmd)
|
||||
return self.c.execute(cmd)
|
||||
return self.cursor.execute(cmd)
|
||||
|
||||
def _fetchall(self):
|
||||
ret = self.c.fetchall()
|
||||
ret = self.cursor.fetchall()
|
||||
logger.debug("fetched: {}".format(ret))
|
||||
return ret
|
||||
|
||||
def _to_sqlite(self, n):
|
||||
assert n >= 0
|
||||
assert n < self.MAX_UINT
|
||||
def _to_sqlite(self, num):
|
||||
assert num >= 0
|
||||
assert num < self.MAX_UINT
|
||||
# NOTE: sqlite stores unit as signed ints, so maximum uint is 2^63-1
|
||||
# see http://jakegoulding.com/blog/2011/02/06/sqlite-64-bit-integers/
|
||||
if n > self.MAX_INT:
|
||||
ret = -(n - self.MAX_INT)
|
||||
if num > self.MAX_INT:
|
||||
ret = -(num - self.MAX_INT)
|
||||
else:
|
||||
ret = n
|
||||
assert self._from_sqlite(ret) == n
|
||||
ret = num
|
||||
assert self._from_sqlite(ret) == num
|
||||
return ret
|
||||
|
||||
def _from_sqlite(self, n):
|
||||
assert abs(n) <= self.MAX_INT
|
||||
if n < 0:
|
||||
return abs(n) + self.MAX_INT
|
||||
assert n < self.MAX_UINT
|
||||
assert n >= 0
|
||||
return n
|
||||
def _from_sqlite(self, num):
|
||||
assert abs(num) <= self.MAX_INT
|
||||
if num < 0:
|
||||
return abs(num) + self.MAX_INT
|
||||
assert num < self.MAX_UINT
|
||||
assert num >= 0
|
||||
return num
|
||||
|
||||
def _prepare_db(self, empty=False):
|
||||
from dvc import VERSION
|
||||
@@ -134,11 +165,7 @@ class State(object):
|
||||
version = ret[0][0]
|
||||
|
||||
if version > self.VERSION:
|
||||
msg = "you are using an old version '{}' of dvc that is " \
|
||||
"using state file version '{}' which is not " \
|
||||
"compatible with the state file version '{}' that " \
|
||||
"is used in this projet. Please upgrade right now!"
|
||||
raise DvcException(msg.format(VERSION, self.VERSION, version))
|
||||
raise StateVersionTooNewError(VERSION, self.VERSION, version)
|
||||
elif version < self.VERSION:
|
||||
msg = "State file version '{}' is too old. " \
|
||||
"Reformatting to the current version '{}'."
|
||||
@@ -166,14 +193,15 @@ class State(object):
|
||||
self._execute(cmd.format(self.VERSION))
|
||||
|
||||
def load(self):
|
||||
"""Loads state database."""
|
||||
retries = 1
|
||||
while True:
|
||||
assert self.db is None
|
||||
assert self.c is None
|
||||
assert self.database is None
|
||||
assert self.cursor is None
|
||||
assert self.inserts == 0
|
||||
empty = not os.path.exists(self.state_file)
|
||||
self.db = sqlite3.connect(self.state_file)
|
||||
self.c = self.db.cursor()
|
||||
self.database = sqlite3.connect(self.state_file)
|
||||
self.cursor = self.database.cursor()
|
||||
|
||||
# Try loading once to check that the file is indeed a database
|
||||
# and reformat it if it is not.
|
||||
@@ -181,10 +209,10 @@ class State(object):
|
||||
self._prepare_db(empty=empty)
|
||||
return
|
||||
except sqlite3.DatabaseError:
|
||||
self.c.close()
|
||||
self.db.close()
|
||||
self.db = None
|
||||
self.c = None
|
||||
self.cursor.close()
|
||||
self.database.close()
|
||||
self.database = None
|
||||
self.cursor = None
|
||||
self.inserts = 0
|
||||
if retries > 0:
|
||||
os.unlink(self.state_file)
|
||||
@@ -194,12 +222,13 @@ class State(object):
|
||||
|
||||
def _vacuum(self):
|
||||
# NOTE: see https://bugs.python.org/issue28518
|
||||
self.db.isolation_level = None
|
||||
self.database.isolation_level = None
|
||||
self._execute("VACUUM")
|
||||
self.db.isolation_level = ''
|
||||
self.database.isolation_level = ''
|
||||
|
||||
def dump(self):
|
||||
assert self.db is not None
|
||||
"""Saves state database."""
|
||||
assert self.database is not None
|
||||
|
||||
cmd = "SELECT count from {} WHERE rowid={}"
|
||||
self._execute(cmd.format(self.STATE_INFO_TABLE,
|
||||
@@ -236,34 +265,34 @@ class State(object):
|
||||
self._to_sqlite(count),
|
||||
self.STATE_INFO_ROW))
|
||||
|
||||
self.db.commit()
|
||||
self.c.close()
|
||||
self.db.close()
|
||||
self.db = None
|
||||
self.c = None
|
||||
self.database.commit()
|
||||
self.cursor.close()
|
||||
self.database.close()
|
||||
self.database = None
|
||||
self.cursor = None
|
||||
self.inserts = 0
|
||||
|
||||
@staticmethod
|
||||
def mtime_and_size(path):
|
||||
def _mtime_and_size(path):
|
||||
size = os.path.getsize(path)
|
||||
mtime = os.path.getmtime(path)
|
||||
|
||||
if os.path.isdir(path):
|
||||
for root, dirs, files in os.walk(path):
|
||||
for name in dirs + files:
|
||||
p = os.path.join(root, name)
|
||||
st = os.stat(p)
|
||||
size += st.st_size
|
||||
m = st.st_mtime
|
||||
if m > mtime:
|
||||
mtime = m
|
||||
entry = os.path.join(root, name)
|
||||
stat = os.stat(entry)
|
||||
size += stat.st_size
|
||||
entry_mtime = stat.st_mtime
|
||||
if entry_mtime > mtime:
|
||||
mtime = entry_mtime
|
||||
|
||||
# State of files handled by dvc is stored in db as TEXT.
|
||||
# We cast results to string for later comparisons with stored values.
|
||||
return str(int(nanotime.timestamp(mtime))), str(size)
|
||||
|
||||
@staticmethod
|
||||
def inode(path):
|
||||
def _inode(path):
|
||||
logger.debug('Path {} inode {}'.format(path, System.inode(path)))
|
||||
return System.inode(path)
|
||||
|
||||
@@ -274,70 +303,98 @@ class State(object):
|
||||
if not os.path.exists(path):
|
||||
return (None, None)
|
||||
|
||||
mtime, size = self.mtime_and_size(path)
|
||||
inode = self.inode(path)
|
||||
actual_mtime, actual_size = self._mtime_and_size(path)
|
||||
actual_inode = self._inode(path)
|
||||
|
||||
cmd = 'SELECT * from {} WHERE inode={}'.format(self.STATE_TABLE,
|
||||
self._to_sqlite(inode))
|
||||
cmd = ('SELECT * from {} WHERE inode={}'
|
||||
.format(self.STATE_TABLE, self._to_sqlite(actual_inode)))
|
||||
|
||||
self._execute(cmd)
|
||||
ret = self._fetchall()
|
||||
if len(ret) == 0:
|
||||
if not ret:
|
||||
md5, info = self._collect(path)
|
||||
cmd = 'INSERT INTO {}(inode, mtime, size, md5, timestamp) ' \
|
||||
'VALUES ({}, "{}", "{}", "{}", "{}")'
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
self._to_sqlite(inode),
|
||||
mtime,
|
||||
size,
|
||||
self._to_sqlite(actual_inode),
|
||||
actual_mtime,
|
||||
actual_size,
|
||||
md5,
|
||||
int(nanotime.timestamp(time.time()))))
|
||||
self.inserts += 1
|
||||
else:
|
||||
assert len(ret) == 1
|
||||
assert len(ret[0]) == 5
|
||||
i, m, s, md5, timestamp = ret[0]
|
||||
i = self._from_sqlite(i)
|
||||
assert i == inode
|
||||
msg = ("Inode '{}', mtime '{}', actual mtime '{}', "
|
||||
"size '{}', actual size '{}'.")
|
||||
logger.debug(msg.format(i, m, mtime, s, size))
|
||||
if mtime != m or size != s:
|
||||
inode, mtime, size, md5, _ = ret[0]
|
||||
inode = self._from_sqlite(inode)
|
||||
assert inode == actual_inode
|
||||
logger.debug(
|
||||
"Inode '{}', mtime '{}', actual mtime '{}', size '{}', "
|
||||
"actual size '{}'."
|
||||
.format(inode, mtime, actual_mtime, size, actual_size))
|
||||
if actual_mtime != mtime or actual_size != size:
|
||||
md5, info = self._collect(path)
|
||||
cmd = ('UPDATE {} SET '
|
||||
'mtime = "{}", size = "{}", '
|
||||
'md5 = "{}", timestamp = "{}" '
|
||||
'WHERE inode = {}')
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
mtime,
|
||||
size,
|
||||
actual_mtime,
|
||||
actual_size,
|
||||
md5,
|
||||
int(nanotime.timestamp(time.time())),
|
||||
self._to_sqlite(inode)))
|
||||
self._to_sqlite(actual_inode)))
|
||||
else:
|
||||
info = None
|
||||
cmd = 'UPDATE {} SET timestamp = "{}" WHERE inode = {}'
|
||||
self._execute(cmd.format(self.STATE_TABLE,
|
||||
int(nanotime.timestamp(time.time())),
|
||||
self._to_sqlite(inode)))
|
||||
self._to_sqlite(actual_inode)))
|
||||
|
||||
return (md5, info)
|
||||
|
||||
def update(self, path):
|
||||
"""Gets the checksum for the specified path. Checksum will be
|
||||
retrieved from the state database if available, otherwise it will be
|
||||
computed and cached in the state database for the further use.
|
||||
|
||||
Args:
|
||||
path (str): path to get the checksum for.
|
||||
|
||||
Returns:
|
||||
str: checksum for the specified path.
|
||||
"""
|
||||
return self._do_update(path)[0]
|
||||
|
||||
def update_info(self, path):
|
||||
"""Gets the checksum and the directory info (if applicable) for the
|
||||
specified path.
|
||||
|
||||
Args:
|
||||
path (str): path to get the checksum and the directory info for.
|
||||
|
||||
Returns:
|
||||
tuple: checksum for the specified path along with a directory info
|
||||
(list of {relative_path: checksum} entries for each file in the
|
||||
directory) if applicable, otherwise None.
|
||||
"""
|
||||
md5, info = self._do_update(path)
|
||||
if not info:
|
||||
info = self.project.cache.local.load_dir_cache(md5)
|
||||
return (md5, info)
|
||||
|
||||
def update_link(self, path):
|
||||
"""Adds the specified path to the list of links created by dvc. This
|
||||
list is later used on `dvc checkout` to cleanup old links.
|
||||
|
||||
Args:
|
||||
path (str): path to add to the list of links.
|
||||
"""
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
mtime, _ = self.mtime_and_size(path)
|
||||
inode = self.inode(path)
|
||||
mtime, _ = self._mtime_and_size(path)
|
||||
inode = self._inode(path)
|
||||
relpath = os.path.relpath(path, self.root_dir)
|
||||
|
||||
cmd = 'REPLACE INTO {}(path, inode, mtime) ' \
|
||||
@@ -348,13 +405,18 @@ class State(object):
|
||||
self._execute(cmd)
|
||||
|
||||
def remove_unused_links(self, used):
|
||||
"""Removes all saved links except the ones that are used.
|
||||
|
||||
Args:
|
||||
used (list): list of used links that should not be removed.
|
||||
"""
|
||||
unused = []
|
||||
|
||||
self._execute('SELECT * FROM {}'.format(self.LINK_STATE_TABLE))
|
||||
for row in self.c:
|
||||
p, i, m = row
|
||||
i = self._from_sqlite(i)
|
||||
path = os.path.join(self.root_dir, p)
|
||||
for row in self.cursor:
|
||||
relpath, inode, mtime = row
|
||||
inode = self._from_sqlite(inode)
|
||||
path = os.path.join(self.root_dir, relpath)
|
||||
|
||||
if path in used:
|
||||
continue
|
||||
@@ -362,14 +424,14 @@ class State(object):
|
||||
if not os.path.exists(path):
|
||||
continue
|
||||
|
||||
inode = self.inode(path)
|
||||
mtime, _ = self.mtime_and_size(path)
|
||||
actual_inode = self._inode(path)
|
||||
actual_mtime, _ = self._mtime_and_size(path)
|
||||
|
||||
if i == inode and m == mtime:
|
||||
if inode == actual_inode and mtime == actual_mtime:
|
||||
logger.debug("Removing '{}' as unused link.".format(path))
|
||||
remove(path)
|
||||
unused.append(p)
|
||||
unused.append(relpath)
|
||||
|
||||
for p in unused:
|
||||
for relpath in unused:
|
||||
cmd = 'DELETE FROM {} WHERE path = "{}"'
|
||||
self._execute(cmd.format(self.LINK_STATE_TABLE, p))
|
||||
self._execute(cmd.format(self.LINK_STATE_TABLE, relpath))
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@ from tests.basic_env import TestDvc
|
||||
|
||||
class TestMtimeAndSize(TestDvc):
|
||||
def test(self):
|
||||
file_time, file_size = State.mtime_and_size(self.DATA)
|
||||
dir_time, dir_size = State.mtime_and_size(self.DATA_DIR)
|
||||
file_time, file_size = State._mtime_and_size(self.DATA)
|
||||
dir_time, dir_size = State._mtime_and_size(self.DATA_DIR)
|
||||
|
||||
actual_file_size = os.path.getsize(self.DATA)
|
||||
actual_dir_size = (os.path.getsize(self.DATA_DIR)
|
||||
|
||||
Reference in New Issue
Block a user