mirror of
https://github.com/Stone-Red-Code/dvc.git
synced 2026-11-09 07:18:27 +01:00
__name__ looks something like `dvc.commands.diff`, but if we are already instantiating root logger with `dvc`, it adds up to create something like `dvc.dvc.commands.diff`. We should be using a root logger here instead.
257 lines
7.4 KiB
Python
257 lines
7.4 KiB
Python
"""This module provides an entrypoint to the dvc cli and parsing utils."""
|
|
|
|
import logging
|
|
import sys
|
|
from typing import Optional
|
|
|
|
from dvc.log import logger
|
|
|
|
# Workaround for CPython bug. See [1] and [2] for more info.
|
|
# [1] https://github.com/aws/aws-cli/blob/1.16.277/awscli/clidriver.py#L55
|
|
# [2] https://bugs.python.org/issue29288
|
|
"".encode("idna")
|
|
|
|
|
|
logger = logger.getChild(__name__)
|
|
|
|
|
|
class DvcParserError(Exception):
|
|
"""Base class for CLI parser errors."""
|
|
|
|
def __init__(self):
|
|
super().__init__("parser error")
|
|
|
|
|
|
def parse_args(argv=None):
|
|
"""Parses CLI arguments.
|
|
|
|
Args:
|
|
argv: optional list of arguments to parse. sys.argv is used by default.
|
|
|
|
Raises:
|
|
DvcParserError: raised for argument parsing errors.
|
|
"""
|
|
from .parser import get_main_parser
|
|
|
|
parser = get_main_parser()
|
|
args = parser.parse_args(argv)
|
|
args.parser = parser
|
|
return args
|
|
|
|
|
|
def _log_unknown_exceptions() -> None:
|
|
from dvc.info import get_dvc_info
|
|
from dvc.ui import ui
|
|
from dvc.utils import colorize
|
|
|
|
logger.exception("unexpected error")
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
logger.debug("Version info for developers:\n%s", get_dvc_info())
|
|
|
|
q = colorize("Having any troubles?", "yellow")
|
|
link = colorize("https://dvc.org/support", "blue")
|
|
footer = f"\n{q} Hit us up at {link}, we are always happy to help!"
|
|
ui.error_write(footer)
|
|
|
|
|
|
def _log_exceptions(exc: Exception) -> Optional[int]:
|
|
"""Try to log some known exceptions, that are not DVCExceptions."""
|
|
from dvc.utils import error_link, format_link
|
|
|
|
if isinstance(exc, OSError):
|
|
import errno
|
|
|
|
if exc.errno == errno.EMFILE:
|
|
logger.exception(
|
|
(
|
|
"too many open files, please visit "
|
|
"%s to see how to handle this problem"
|
|
),
|
|
error_link("many-files"),
|
|
extra={"tb_only": True},
|
|
)
|
|
else:
|
|
_log_unknown_exceptions()
|
|
return None
|
|
|
|
from dvc.fs import AuthError, ConfigError, RemoteMissingDepsError
|
|
|
|
if isinstance(exc, RemoteMissingDepsError):
|
|
from dvc import PKG
|
|
|
|
proto = exc.protocol
|
|
by_pkg = {
|
|
"pip": f"pip install 'dvc[{proto}]'",
|
|
"conda": f"conda install -c conda-forge dvc-{proto}",
|
|
}
|
|
|
|
if PKG in by_pkg:
|
|
link = format_link("https://dvc.org/doc/install")
|
|
cmd = by_pkg.get(PKG)
|
|
hint = (
|
|
"To install dvc with those dependencies, run:\n"
|
|
"\n"
|
|
f"\t{cmd}\n"
|
|
"\n"
|
|
f"See {link} for more info."
|
|
)
|
|
else:
|
|
link = format_link("https://github.com/iterative/dvc/issues")
|
|
hint = f"\nPlease report this bug to {link}. Thank you!"
|
|
|
|
logger.exception(
|
|
"URL '%s' is supported but requires these missing dependencies: %s. %s",
|
|
exc.url,
|
|
exc.missing_deps,
|
|
hint,
|
|
extra={"tb_only": True},
|
|
)
|
|
return None
|
|
|
|
if isinstance(exc, (AuthError, ConfigError)):
|
|
link = format_link("https://man.dvc.org/remote/modify")
|
|
logger.exception("configuration error")
|
|
logger.exception(
|
|
"%s\nLearn more about configuration settings at %s.",
|
|
exc,
|
|
link,
|
|
extra={"tb_only": True},
|
|
)
|
|
return 251
|
|
|
|
from dvc_data.hashfile.cache import DiskError
|
|
|
|
if isinstance(exc, DiskError):
|
|
from dvc.utils import relpath
|
|
|
|
directory = relpath(exc.directory)
|
|
logger.exception(
|
|
(
|
|
"Could not open pickled '%s' cache.\n"
|
|
"Remove the '%s' directory and then retry this command."
|
|
"\nSee %s for more information."
|
|
),
|
|
exc.type,
|
|
directory,
|
|
error_link("pickle"),
|
|
extra={"tb_only": True},
|
|
)
|
|
return None
|
|
|
|
from dvc_data.hashfile.build import IgnoreInCollectedDirError
|
|
|
|
if isinstance(exc, IgnoreInCollectedDirError):
|
|
logger.exception("")
|
|
return None
|
|
|
|
_log_unknown_exceptions()
|
|
return None
|
|
|
|
|
|
def main(argv=None): # noqa: C901, PLR0912, PLR0915
|
|
"""Main entry point for dvc CLI.
|
|
|
|
Args:
|
|
argv: optional list of arguments to parse. sys.argv is used by default.
|
|
|
|
Returns:
|
|
int: command's return code.
|
|
"""
|
|
from dvc._debug import debugtools
|
|
from dvc.config import ConfigError
|
|
from dvc.exceptions import DvcException, NotDvcRepoError
|
|
from dvc.logger import set_loggers_level
|
|
|
|
# NOTE: stderr/stdout may be closed if we are running from dvc.daemon.
|
|
# On Linux we directly call cli.main after double forking and closing
|
|
# the copied parent's standard file descriptors. If we make any logging
|
|
# calls in this state it will cause an exception due to writing to a closed
|
|
# file descriptor.
|
|
if not sys.stderr or sys.stderr.closed:
|
|
logging.disable()
|
|
elif not sys.stdout or sys.stdout.closed:
|
|
logging.disable(logging.INFO)
|
|
|
|
args = None
|
|
|
|
outer_log_level = logger.level
|
|
level = None
|
|
try:
|
|
args = parse_args(argv)
|
|
|
|
if args.quiet:
|
|
level = logging.CRITICAL
|
|
elif args.verbose == 1:
|
|
level = logging.DEBUG
|
|
elif args.verbose > 1:
|
|
level = logging.TRACE # type: ignore[attr-defined]
|
|
|
|
if level is not None:
|
|
set_loggers_level(level)
|
|
|
|
if level and level <= logging.DEBUG:
|
|
from platform import platform, python_implementation, python_version
|
|
|
|
from dvc import PKG, __version__
|
|
|
|
pyv = f"{python_implementation()} {python_version()}"
|
|
pkg = f" ({PKG})" if PKG else ""
|
|
logger.debug("v%s%s, %s on %s", __version__, pkg, pyv, platform())
|
|
logger.debug("command: %s", " ".join(argv or sys.argv))
|
|
|
|
logger.trace(args)
|
|
|
|
if sys.stdout and not sys.stdout.closed and not args.quiet:
|
|
from dvc.ui import ui
|
|
|
|
ui.enable()
|
|
|
|
with debugtools(args):
|
|
cmd = args.func(args)
|
|
ret = cmd.do_run()
|
|
except ConfigError:
|
|
logger.exception("configuration error")
|
|
ret = 251
|
|
except KeyboardInterrupt:
|
|
logger.exception("interrupted by the user")
|
|
ret = 252
|
|
except BrokenPipeError:
|
|
import os
|
|
|
|
# Python flushes standard streams on exit; redirect remaining output
|
|
# to devnull to avoid another BrokenPipeError at shutdown
|
|
# See: https://docs.python.org/3/library/signal.html#note-on-sigpipe
|
|
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
os.dup2(devnull, sys.stdout.fileno())
|
|
ret = 141 # 128 + 13 (SIGPIPE)
|
|
except NotDvcRepoError:
|
|
logger.exception("")
|
|
ret = 253
|
|
except DvcException:
|
|
ret = 255
|
|
logger.exception("")
|
|
except DvcParserError:
|
|
ret = 254
|
|
except Exception as exc: # noqa: BLE001
|
|
ret = _log_exceptions(exc) or 255
|
|
|
|
try:
|
|
import os
|
|
|
|
from dvc import analytics
|
|
|
|
if analytics.is_enabled():
|
|
analytics.collect_and_send_report(args, ret)
|
|
|
|
logger.trace("Process %s exiting with %s", os.getpid(), ret)
|
|
|
|
return ret
|
|
finally:
|
|
logger.setLevel(outer_log_level)
|
|
|
|
from dvc.repo.open_repo import clean_repos
|
|
|
|
# Remove cached repos in the end of the call, these are anonymous
|
|
# so won't be reused by any other subsequent run anyway.
|
|
clean_repos()
|