Files
dvc/dvc/utils/compat.py
T
Ruslan Kuprieiev e384685715 dvc: release locks when running a command
Currently only one instance of dvc could run in a particular repo
because of the `.dvc/lock`. That gets furstrating when you are running
some long-running command and blocked from doing anything else.
This PR solved that issue for `dvc run` by releasing the repo lock
while running the command. To protect against conflicts, we now use
rwlock, which keeps the list of readers and writers for particular
paths. The process looks like this:

1) aquire read locks for deps;
2) aquire write locks for outs;
3) release repo lock;
4) run cmd;
5) aquire repo lock;
6) release write locks for outs;
7) release read locks for outs;

We also take rwlocks for operations such as `import`, `add` and `checkout`
to ensure that we won't hit the same files that are being used by
another `dvc run` in the background.

We don't aquire rwlocks for the stage file itself, as we don't really
care about anyone overwriting a stage file, as there is a very little
difference here between parallel runs and sequential runs that overwrite
the same stage file.

Next steps might be to release repo lock on `import`(when dowloading
stuff) and when computing checksums or copying files.

Fixes #755
2019-12-13 10:38:10 +02:00

226 lines
6.4 KiB
Python

"""Handle import compatibility between Python 2 and Python 3"""
from __future__ import absolute_import
import errno
import os
import sys
from contextlib import contextmanager
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = _ver[0] == 2
#: Python 3.x?
is_py3 = _ver[0] == 3
# simplified version of ipython_genutils/encoding.py
DEFAULT_ENCODING = sys.getdefaultencoding()
if _ver[:2] < (3, 5):
RecursionError = RuntimeError
else:
RecursionError = RecursionError
def no_code(x, encoding=None):
return x
def encode(u, encoding=None):
encoding = encoding or DEFAULT_ENCODING
return u.encode(encoding, "replace")
def csv_reader(unicode_csv_data, dialect=None, **kwargs):
"""csv.reader doesn't support Unicode input, so need to use some tricks
to work around this.
Source: https://docs.python.org/2/library/csv.html#csv-examples
"""
import csv
dialect = dialect or csv.excel
if is_py3:
# Python3 supports encoding by default, so just return the object
for row in csv.reader(unicode_csv_data, dialect=dialect, **kwargs):
yield [cell for cell in row]
else:
# csv.py doesn't do Unicode; encode temporarily as UTF-8:
reader = csv.reader(
utf_8_encoder(unicode_csv_data), dialect=dialect, **kwargs
)
for row in reader:
# decode UTF-8 back to Unicode, cell by cell:
yield [unicode(cell, "utf-8") for cell in row] # noqa: F821
def utf_8_encoder(unicode_csv_data):
"""Source: https://docs.python.org/2/library/csv.html#csv-examples"""
for line in unicode_csv_data:
yield line.encode("utf-8")
def cast_bytes(s, encoding=None):
"""Source: https://github.com/ipython/ipython_genutils"""
if not isinstance(s, bytes):
return encode(s, encoding)
return s
def _makedirs(name, mode=0o777, exist_ok=False):
"""Source: https://github.com/python/cpython/blob/
3ce3dea60646d8a5a1c952469a2eb65f937875b3/Lib/os.py#L196-L226
"""
head, tail = os.path.split(name)
if not tail:
head, tail = os.path.split(head)
if head and tail and not os.path.exists(head):
try:
_makedirs(head, exist_ok=exist_ok)
except OSError as e:
if e.errno != errno.EEXIST:
raise
cdir = os.curdir
if tail == cdir:
return
try:
os.mkdir(name, mode)
except OSError:
if not exist_ok or not os.path.isdir(name):
raise
@contextmanager
def ignore_file_not_found():
try:
yield
except IOError as exc:
if exc.errno != errno.ENOENT:
raise
if is_py2:
from urlparse import urlparse, urlunparse, urljoin # noqa: F401
from urllib import urlencode # noqa: F401
import ConfigParser # noqa: F401
from io import open # noqa: F401
import pathlib2 as pathlib # noqa: F401
from collections import Mapping # noqa: F401
from contextlib2 import ExitStack # noqa: F401
builtin_str = str # noqa: F821
bytes = str # noqa: F821
str = unicode # noqa: F821
basestring = basestring # noqa: F821
numeric_types = (int, long, float) # noqa: F821
integer_types = (int, long) # noqa: F821
input = raw_input # noqa: F821
cast_bytes_py2 = cast_bytes
makedirs = _makedirs
range = xrange # noqa: F821
FileNotFoundError = IOError
JSONDecodeError = ValueError
import StringIO
import io
class StringIO(StringIO.StringIO):
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
class BytesIO(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def convert_to_unicode(data):
if isinstance(data, builtin_str):
return str(data)
if isinstance(data, dict):
return dict(map(convert_to_unicode, data.items()))
if isinstance(data, (list, tuple)):
return type(data)(map(convert_to_unicode, data))
return data
elif is_py3:
import pathlib # noqa: F401
from os import makedirs # noqa: F401
from urllib.parse import ( # noqa: F401
urlparse, # noqa: F401
urlunparse, # noqa: F401
urlencode, # noqa: F401
urljoin, # noqa: F401
)
from io import StringIO, BytesIO # noqa: F401
import configparser as ConfigParser # noqa: F401
from collections.abc import Mapping # noqa: F401
from contextlib import ExitStack # noqa: F401
from json.decoder import JSONDecodeError # noqa: F401
builtin_str = str # noqa: F821
str = str # noqa: F821
bytes = bytes # noqa: F821
basestring = (str, bytes) # noqa: F821
numeric_types = (int, float) # noqa: F821
integer_types = (int,) # noqa: F821
input = input # noqa: F821
open = open # noqa: F821
cast_bytes_py2 = no_code
range = range # noqa: F821
FileNotFoundError = FileNotFoundError
def convert_to_unicode(data):
return data
# Backport os.fspath() from Python 3.6
try:
from os import fspath # noqa: F821
fspath_py35 = lambda s: s # noqa: E731
except ImportError:
def fspath(path):
"""Return the path representation of a path-like object.
If str or bytes is passed in, it is returned unchanged. Otherwise the
os.PathLike interface is used to get the path representation. If the
path representation is not str or bytes, TypeError is raised. If the
provided path is not str, bytes, or os.PathLike, TypeError is raised.
"""
if isinstance(path, (str, bytes)):
return path
# Work from the object's type to match method resolution of other magic
# methods.
path_type = type(path)
try:
path_repr = path_type.__fspath__(path)
except AttributeError:
if hasattr(path_type, "__fspath__"):
raise
else:
raise TypeError(
"expected str, bytes or os.PathLike object, "
"not " + path_type.__name__
)
if isinstance(path_repr, (str, bytes)):
return path_repr
else:
raise TypeError(
"expected {}.__fspath__() to return str or bytes, "
"not {}".format(path_type.__name__, type(path_repr).__name__)
)
fspath_py35 = fspath