[pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci
This commit is contained in:
pre-commit-ci[bot] 2024-04-13 00:00:18 +00:00
parent 72ad6dc953
commit f4cd1ba0d6
813 changed files with 66015 additions and 58839 deletions

View file

@ -1,4 +1,4 @@
"""Subpackage containing all of pip's command line interface related code
"""
# This file intentionally does not import submodules
from __future__ import annotations

View file

@ -1,35 +1,40 @@
"""Logic that powers autocompletion installed by ``pip completion``.
"""
from __future__ import annotations
import optparse
import os
import sys
from itertools import chain
from typing import Any, Iterable, List, Optional
from typing import Any
from typing import Iterable
from typing import List
from typing import Optional
from pip._internal.cli.main_parser import create_main_parser
from pip._internal.commands import commands_dict, create_command
from pip._internal.commands import commands_dict
from pip._internal.commands import create_command
from pip._internal.metadata import get_default_environment
def autocomplete() -> None:
"""Entry Point for completion of main and subcommand options."""
# Don't complete if user hasn't sourced bash_completion file.
if "PIP_AUTO_COMPLETE" not in os.environ:
if 'PIP_AUTO_COMPLETE' not in os.environ:
return
cwords = os.environ["COMP_WORDS"].split()[1:]
cword = int(os.environ["COMP_CWORD"])
cwords = os.environ['COMP_WORDS'].split()[1:]
cword = int(os.environ['COMP_CWORD'])
try:
current = cwords[cword - 1]
except IndexError:
current = ""
current = ''
parser = create_main_parser()
subcommands = list(commands_dict)
options = []
# subcommand
subcommand_name: Optional[str] = None
subcommand_name: str | None = None
for word in cwords:
if word in subcommands:
subcommand_name = word
@ -37,12 +42,12 @@ def autocomplete() -> None:
# subcommand options
if subcommand_name is not None:
# special case: 'help' subcommand has no options
if subcommand_name == "help":
if subcommand_name == 'help':
sys.exit(1)
# special case: list locally installed dists for show and uninstall
should_list_installed = not current.startswith("-") and subcommand_name in [
"show",
"uninstall",
should_list_installed = not current.startswith('-') and subcommand_name in [
'show',
'uninstall',
]
if should_list_installed:
env = get_default_environment()
@ -50,8 +55,8 @@ def autocomplete() -> None:
installed = [
dist.canonical_name
for dist in env.iter_installed_distributions(local_only=True)
if dist.canonical_name.startswith(lc)
and dist.canonical_name not in cwords[1:]
if dist.canonical_name.startswith(lc) and
dist.canonical_name not in cwords[1:]
]
# if there are no dists installed, fall back to option completion
if installed:
@ -60,10 +65,10 @@ def autocomplete() -> None:
sys.exit(1)
should_list_installables = (
not current.startswith("-") and subcommand_name == "install"
not current.startswith('-') and subcommand_name == 'install'
)
if should_list_installables:
for path in auto_complete_paths(current, "path"):
for path in auto_complete_paths(current, 'path'):
print(path)
sys.exit(1)
@ -75,7 +80,7 @@ def autocomplete() -> None:
options.append((opt_str, opt.nargs))
# filter out previously specified options from available options
prev_opts = [x.split("=")[0] for x in cwords[1 : cword - 1]]
prev_opts = [x.split('=')[0] for x in cwords[1: cword - 1]]
options = [(x, v) for (x, v) in options if x not in prev_opts]
# filter options by current input
options = [(k, v) for k, v in options if k.startswith(current)]
@ -93,8 +98,8 @@ def autocomplete() -> None:
for option in options:
opt_label = option[0]
# append '=' to options which require args
if option[1] and option[0][:2] == "--":
opt_label += "="
if option[1] and option[0][:2] == '--':
opt_label += '='
print(opt_label)
else:
# show main parser options only when necessary
@ -102,7 +107,7 @@ def autocomplete() -> None:
opts = [i.option_list for i in parser.option_groups]
opts.append(parser.option_list)
flattened_opts = chain.from_iterable(opts)
if current.startswith("-"):
if current.startswith('-'):
for opt in flattened_opts:
if opt.help != optparse.SUPPRESS_HELP:
subcommands += opt._long_opts + opt._short_opts
@ -112,13 +117,13 @@ def autocomplete() -> None:
if completion_type:
subcommands = list(auto_complete_paths(current, completion_type))
print(" ".join([x for x in subcommands if x.startswith(current)]))
print(' '.join([x for x in subcommands if x.startswith(current)]))
sys.exit(1)
def get_path_completion_type(
cwords: List[str], cword: int, opts: Iterable[Any]
) -> Optional[str]:
cwords: list[str], cword: int, opts: Iterable[Any],
) -> str | None:
"""Get the type of path completion (``file``, ``dir``, ``path`` or None)
:param cwords: same as the environmental variable ``COMP_WORDS``
@ -126,15 +131,15 @@ def get_path_completion_type(
:param opts: The available options to check
:return: path completion type (``file``, ``dir``, ``path`` or None)
"""
if cword < 2 or not cwords[cword - 2].startswith("-"):
if cword < 2 or not cwords[cword - 2].startswith('-'):
return None
for opt in opts:
if opt.help == optparse.SUPPRESS_HELP:
continue
for o in str(opt).split("/"):
if cwords[cword - 2].split("=")[0] == o:
for o in str(opt).split('/'):
if cwords[cword - 2].split('=')[0] == o:
if not opt.metavar or any(
x in ("path", "file", "dir") for x in opt.metavar.split("/")
x in ('path', 'file', 'dir') for x in opt.metavar.split('/')
):
return opt.metavar
return None
@ -165,7 +170,7 @@ def auto_complete_paths(current: str, completion_type: str) -> Iterable[str]:
# complete regular files when there is not ``<dir>`` after option
# complete directories when there is ``<file>``, ``<path>`` or
# ``<dir>``after option
if completion_type != "dir" and os.path.isfile(opt):
if completion_type != 'dir' and os.path.isfile(opt):
yield comp_file
elif os.path.isdir(opt):
yield os.path.join(comp_file, "")
yield os.path.join(comp_file, '')

View file

@ -1,49 +1,52 @@
"""Base Command class, and related routines"""
from __future__ import annotations
import functools
import logging
import logging.config
import optparse
import os
import sys
import traceback
from optparse import Values
from typing import Any, Callable, List, Optional, Tuple
from pip._vendor.rich import traceback as rich_traceback
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.cli import cmdoptions
from pip._internal.cli.command_context import CommandContextMixIn
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.cli.status_codes import (
ERROR,
PREVIOUS_BUILD_DIR_ERROR,
UNKNOWN_ERROR,
VIRTUALENV_NOT_FOUND,
)
from pip._internal.exceptions import (
BadCommand,
CommandError,
DiagnosticPipError,
InstallationError,
NetworkConnectionError,
PreviousBuildDirError,
UninstallationError,
)
from pip._internal.cli.parser import ConfigOptionParser
from pip._internal.cli.parser import UpdatingDefaultsHelpFormatter
from pip._internal.cli.status_codes import ERROR
from pip._internal.cli.status_codes import PREVIOUS_BUILD_DIR_ERROR
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.cli.status_codes import VIRTUALENV_NOT_FOUND
from pip._internal.exceptions import BadCommand
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import DiagnosticPipError
from pip._internal.exceptions import InstallationError
from pip._internal.exceptions import NetworkConnectionError
from pip._internal.exceptions import PreviousBuildDirError
from pip._internal.exceptions import UninstallationError
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging
from pip._internal.utils.misc import get_prog, normalize_path
from pip._internal.utils.logging import BrokenStdoutLoggingError
from pip._internal.utils.logging import setup_logging
from pip._internal.utils.misc import get_prog
from pip._internal.utils.misc import normalize_path
from pip._internal.utils.temp_dir import global_tempdir_manager
from pip._internal.utils.temp_dir import tempdir_registry
from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry as TempDirRegistry
from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry
from pip._internal.utils.virtualenv import running_under_virtualenv
from pip._vendor.rich import traceback as rich_traceback
__all__ = ["Command"]
__all__ = ['Command']
logger = logging.getLogger(__name__)
class Command(CommandContextMixIn):
usage: str = ""
usage: str = ''
ignore_require_venv: bool = False
def __init__(self, name: str, summary: str, isolated: bool = False) -> None:
@ -53,7 +56,7 @@ class Command(CommandContextMixIn):
self.summary = summary
self.parser = ConfigOptionParser(
usage=self.usage,
prog=f"{get_prog()} {name}",
prog=f'{get_prog()} {name}',
formatter=UpdatingDefaultsHelpFormatter(),
add_help_option=False,
name=name,
@ -61,10 +64,10 @@ class Command(CommandContextMixIn):
isolated=isolated,
)
self.tempdir_registry: Optional[TempDirRegistry] = None
self.tempdir_registry: TempDirRegistry | None = None
# Commands should add options to this option group
optgroup_name = f"{self.name.capitalize()} Options"
optgroup_name = f'{self.name.capitalize()} Options'
self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name)
# Add the general options
@ -86,23 +89,23 @@ class Command(CommandContextMixIn):
"""
# Make sure we do the pip version check if the index_group options
# are present.
assert not hasattr(options, "no_index")
assert not hasattr(options, 'no_index')
def run(self, options: Values, args: List[str]) -> int:
def run(self, options: Values, args: list[str]) -> int:
raise NotImplementedError
def parse_args(self, args: List[str]) -> Tuple[Values, List[str]]:
def parse_args(self, args: list[str]) -> tuple[Values, list[str]]:
# factored out for testability
return self.parser.parse_args(args)
def main(self, args: List[str]) -> int:
def main(self, args: list[str]) -> int:
try:
with self.main_context():
return self._main(args)
finally:
logging.shutdown()
def _main(self, args: List[str]) -> int:
def _main(self, args: list[str]) -> int:
# We must initialize this before the tempdir manager, otherwise the
# configuration would not be accessible by the time we clean up the
# tempdir manager.
@ -127,15 +130,15 @@ class Command(CommandContextMixIn):
# This also affects isolated builds and it should.
if options.no_input:
os.environ["PIP_NO_INPUT"] = "1"
os.environ['PIP_NO_INPUT'] = '1'
if options.exists_action:
os.environ["PIP_EXISTS_ACTION"] = " ".join(options.exists_action)
os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action)
if options.require_venv and not self.ignore_require_venv:
# If a venv is required check if it can really be found
if not running_under_virtualenv():
logger.critical("Could not find an activated virtualenv (required).")
logger.critical('Could not find an activated virtualenv (required).')
sys.exit(VIRTUALENV_NOT_FOUND)
if options.cache_dir:
@ -143,23 +146,23 @@ class Command(CommandContextMixIn):
if not check_path_owner(options.cache_dir):
logger.warning(
"The directory '%s' or its parent directory is not owned "
"or is not writable by the current user. The cache "
"has been disabled. Check the permissions and owner of "
"that directory. If executing pip with sudo, you should "
'or is not writable by the current user. The cache '
'has been disabled. Check the permissions and owner of '
'that directory. If executing pip with sudo, you should '
"use sudo's -H flag.",
options.cache_dir,
)
options.cache_dir = None
if "2020-resolver" in options.features_enabled:
if '2020-resolver' in options.features_enabled:
logger.warning(
"--use-feature=2020-resolver no longer has any effect, "
"since it is now the default dependency resolver in pip. "
"This will become an error in pip 21.0."
'--use-feature=2020-resolver no longer has any effect, '
'since it is now the default dependency resolver in pip. '
'This will become an error in pip 21.0.',
)
def intercepts_unhandled_exc(
run_func: Callable[..., int]
run_func: Callable[..., int],
) -> Callable[..., int]:
@functools.wraps(run_func)
def exc_logging_wrapper(*args: Any) -> int:
@ -168,13 +171,13 @@ class Command(CommandContextMixIn):
assert isinstance(status, int)
return status
except DiagnosticPipError as exc:
logger.error("[present-diagnostic] %s", exc)
logger.debug("Exception information:", exc_info=True)
logger.error('[present-diagnostic] %s', exc)
logger.debug('Exception information:', exc_info=True)
return ERROR
except PreviousBuildDirError as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
logger.debug('Exception information:', exc_info=True)
return PREVIOUS_BUILD_DIR_ERROR
except (
@ -184,29 +187,29 @@ class Command(CommandContextMixIn):
NetworkConnectionError,
) as exc:
logger.critical(str(exc))
logger.debug("Exception information:", exc_info=True)
logger.debug('Exception information:', exc_info=True)
return ERROR
except CommandError as exc:
logger.critical("%s", exc)
logger.debug("Exception information:", exc_info=True)
logger.critical('%s', exc)
logger.debug('Exception information:', exc_info=True)
return ERROR
except BrokenStdoutLoggingError:
# Bypass our logger and write any remaining messages to
# stderr because stdout no longer works.
print("ERROR: Pipe to stdout was broken", file=sys.stderr)
print('ERROR: Pipe to stdout was broken', file=sys.stderr)
if level_number <= logging.DEBUG:
traceback.print_exc(file=sys.stderr)
return ERROR
except KeyboardInterrupt:
logger.critical("Operation cancelled by user")
logger.debug("Exception information:", exc_info=True)
logger.critical('Operation cancelled by user')
logger.debug('Exception information:', exc_info=True)
return ERROR
except BaseException:
logger.critical("Exception:", exc_info=True)
logger.critical('Exception:', exc_info=True)
return UNKNOWN_ERROR

View file

@ -1,7 +1,12 @@
from contextlib import ExitStack, contextmanager
from typing import ContextManager, Iterator, TypeVar
from __future__ import annotations
_T = TypeVar("_T", covariant=True)
from contextlib import contextmanager
from contextlib import ExitStack
from typing import ContextManager
from typing import Iterator
from typing import TypeVar
_T = TypeVar('_T', covariant=True)
class CommandContextMixIn:

View file

@ -1,10 +1,13 @@
"""Primary application entrypoint.
"""
from __future__ import annotations
import locale
import logging
import os
import sys
from typing import List, Optional
from typing import List
from typing import Optional
from pip._internal.cli.autocompletion import autocomplete
from pip._internal.cli.main_parser import parse_command
@ -42,7 +45,7 @@ logger = logging.getLogger(__name__)
# main, this should not be an issue in practice.
def main(args: Optional[List[str]] = None) -> int:
def main(args: list[str] | None = None) -> int:
if args is None:
args = sys.argv[1:]
@ -54,17 +57,17 @@ def main(args: Optional[List[str]] = None) -> int:
try:
cmd_name, cmd_args = parse_command(args)
except PipError as exc:
sys.stderr.write(f"ERROR: {exc}")
sys.stderr.write(f'ERROR: {exc}')
sys.stderr.write(os.linesep)
sys.exit(1)
# Needed for locale.getpreferredencoding(False) to work
# in pip._internal.utils.encoding.auto_decode
try:
locale.setlocale(locale.LC_ALL, "")
locale.setlocale(locale.LC_ALL, '')
except locale.Error as e:
# setlocale can apparently crash if locale are uninitialized
logger.debug("Ignoring error %s when setting locale", e)
command = create_command(cmd_name, isolated=("--isolated" in cmd_args))
logger.debug('Ignoring error %s when setting locale', e)
command = create_command(cmd_name, isolated=('--isolated' in cmd_args))
return command.main(cmd_args)

View file

@ -1,27 +1,32 @@
"""A single place for constructing and exposing the main parser
"""
from __future__ import annotations
import os
import sys
from typing import List, Tuple
from typing import List
from typing import Tuple
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict, get_similar_commands
from pip._internal.cli.parser import ConfigOptionParser
from pip._internal.cli.parser import UpdatingDefaultsHelpFormatter
from pip._internal.commands import commands_dict
from pip._internal.commands import get_similar_commands
from pip._internal.exceptions import CommandError
from pip._internal.utils.misc import get_pip_version, get_prog
from pip._internal.utils.misc import get_pip_version
from pip._internal.utils.misc import get_prog
__all__ = ["create_main_parser", "parse_command"]
__all__ = ['create_main_parser', 'parse_command']
def create_main_parser() -> ConfigOptionParser:
"""Creates and returns the main parser for pip's CLI"""
parser = ConfigOptionParser(
usage="\n%prog <command> [options]",
usage='\n%prog <command> [options]',
add_help_option=False,
formatter=UpdatingDefaultsHelpFormatter(),
name="global",
name='global',
prog=get_prog(),
)
parser.disable_interspersed_args()
@ -36,16 +41,16 @@ def create_main_parser() -> ConfigOptionParser:
parser.main = True # type: ignore
# create command listing for description
description = [""] + [
f"{name:27} {command_info.summary}"
description = [''] + [
f'{name:27} {command_info.summary}'
for name, command_info in commands_dict.items()
]
parser.description = "\n".join(description)
parser.description = '\n'.join(description)
return parser
def parse_command(args: List[str]) -> Tuple[str, List[str]]:
def parse_command(args: list[str]) -> tuple[str, list[str]]:
parser = create_main_parser()
# Note: parser calls disable_interspersed_args(), so the result of this
@ -64,7 +69,7 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
sys.exit()
# pip || pip help -> print_help()
if not args_else or (args_else[0] == "help" and len(args_else) == 1):
if not args_else or (args_else[0] == 'help' and len(args_else) == 1):
parser.print_help()
sys.exit()
@ -78,7 +83,7 @@ def parse_command(args: List[str]) -> Tuple[str, List[str]]:
if guess:
msg.append(f'maybe you meant "{guess}"')
raise CommandError(" - ".join(msg))
raise CommandError(' - '.join(msg))
# all the args without the subcommand
cmd_args = args[:]

View file

@ -1,4 +1,5 @@
"""Base option parser setup"""
from __future__ import annotations
import logging
import optparse
@ -6,11 +7,17 @@ import shutil
import sys
import textwrap
from contextlib import suppress
from typing import Any, Dict, Iterator, List, Tuple
from typing import Any
from typing import Dict
from typing import Iterator
from typing import List
from typing import Tuple
from pip._internal.cli.status_codes import UNKNOWN_ERROR
from pip._internal.configuration import Configuration, ConfigurationError
from pip._internal.utils.misc import redact_auth_from_url, strtobool
from pip._internal.configuration import Configuration
from pip._internal.configuration import ConfigurationError
from pip._internal.utils.misc import redact_auth_from_url
from pip._internal.utils.misc import strtobool
logger = logging.getLogger(__name__)
@ -20,16 +27,16 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
def __init__(self, *args: Any, **kwargs: Any) -> None:
# help position must be aligned with __init__.parseopts.description
kwargs["max_help_position"] = 30
kwargs["indent_increment"] = 1
kwargs["width"] = shutil.get_terminal_size()[0] - 2
kwargs['max_help_position'] = 30
kwargs['indent_increment'] = 1
kwargs['width'] = shutil.get_terminal_size()[0] - 2
super().__init__(*args, **kwargs)
def format_option_strings(self, option: optparse.Option) -> str:
return self._format_option_strings(option)
def _format_option_strings(
self, option: optparse.Option, mvarfmt: str = " <{}>", optsep: str = ", "
self, option: optparse.Option, mvarfmt: str = ' <{}>', optsep: str = ', ',
) -> str:
"""
Return a comma-separated list of option strings and metavars.
@ -52,49 +59,49 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
metavar = option.metavar or option.dest.lower()
opts.append(mvarfmt.format(metavar.lower()))
return "".join(opts)
return ''.join(opts)
def format_heading(self, heading: str) -> str:
if heading == "Options":
return ""
return heading + ":\n"
if heading == 'Options':
return ''
return heading + ':\n'
def format_usage(self, usage: str) -> str:
"""
Ensure there is only one newline between usage and the first heading
if there is no description.
"""
msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " "))
msg = '\nUsage: {}\n'.format(self.indent_lines(textwrap.dedent(usage), ' '))
return msg
def format_description(self, description: str) -> str:
# leave full control over description to us
if description:
if hasattr(self.parser, "main"):
label = "Commands"
if hasattr(self.parser, 'main'):
label = 'Commands'
else:
label = "Description"
label = 'Description'
# some doc strings have initial newlines, some don't
description = description.lstrip("\n")
description = description.lstrip('\n')
# some doc strings have final newlines and spaces, some don't
description = description.rstrip()
# dedent, then reindent
description = self.indent_lines(textwrap.dedent(description), " ")
description = f"{label}:\n{description}\n"
description = self.indent_lines(textwrap.dedent(description), ' ')
description = f'{label}:\n{description}\n'
return description
else:
return ""
return ''
def format_epilog(self, epilog: str) -> str:
# leave full control over epilog to us
if epilog:
return epilog
else:
return ""
return ''
def indent_lines(self, text: str, indent: str) -> str:
new_lines = [indent + line for line in text.split("\n")]
return "\n".join(new_lines)
new_lines = [indent + line for line in text.split('\n')]
return '\n'.join(new_lines)
class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
@ -115,7 +122,7 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
default_values = self.parser.defaults.get(option.dest)
help_text = super().expand_default(option)
if default_values and option.metavar == "URL":
if default_values and option.metavar == 'URL':
if isinstance(default_values, str):
default_values = [default_values]
@ -131,7 +138,7 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter):
class CustomOptionParser(optparse.OptionParser):
def insert_option_group(
self, idx: int, *args: Any, **kwargs: Any
self, idx: int, *args: Any, **kwargs: Any,
) -> optparse.OptionGroup:
"""Insert an OptionGroup at a given position."""
group = self.add_option_group(*args, **kwargs)
@ -142,7 +149,7 @@ class CustomOptionParser(optparse.OptionParser):
return group
@property
def option_list_all(self) -> List[optparse.Option]:
def option_list_all(self) -> list[optparse.Option]:
"""Get a list of all options, including those in option groups."""
res = self.option_list[:]
for i in self.option_groups:
@ -172,15 +179,15 @@ class ConfigOptionParser(CustomOptionParser):
try:
return option.check_value(key, val)
except optparse.OptionValueError as exc:
print(f"An error occurred during configuration: {exc}")
print(f'An error occurred during configuration: {exc}')
sys.exit(3)
def _get_ordered_configuration_items(self) -> Iterator[Tuple[str, Any]]:
def _get_ordered_configuration_items(self) -> Iterator[tuple[str, Any]]:
# Configuration gives keys in an unordered manner. Order them.
override_order = ["global", self.name, ":env:"]
override_order = ['global', self.name, ':env:']
# Pool the options into different groups
section_items: Dict[str, List[Tuple[str, Any]]] = {
section_items: dict[str, list[tuple[str, Any]]] = {
name: [] for name in override_order
}
for section_key, val in self.config.items():
@ -192,7 +199,7 @@ class ConfigOptionParser(CustomOptionParser):
)
continue
section, key = section_key.split(".", 1)
section, key = section_key.split('.', 1)
if section in override_order:
section_items[section].append((key, val))
@ -201,7 +208,7 @@ class ConfigOptionParser(CustomOptionParser):
for key, val in section_items[section]:
yield key, val
def _update_defaults(self, defaults: Dict[str, Any]) -> Dict[str, Any]:
def _update_defaults(self, defaults: dict[str, Any]) -> dict[str, Any]:
"""Updates the given defaults with values from the config files and
the environ. Does a little special handling for certain types of
options (lists)."""
@ -212,7 +219,7 @@ class ConfigOptionParser(CustomOptionParser):
# Then set the options with those values
for key, val in self._get_ordered_configuration_items():
# '--' because configuration supports only long names
option = self.get_option("--" + key)
option = self.get_option('--' + key)
# Ignore options not present in this parser. E.g. non-globals put
# in [global] by users that want them to apply to all applicable
@ -222,31 +229,31 @@ class ConfigOptionParser(CustomOptionParser):
assert option.dest is not None
if option.action in ("store_true", "store_false"):
if option.action in ('store_true', 'store_false'):
try:
val = strtobool(val)
except ValueError:
self.error(
"{} is not a valid value for {} option, " # noqa
"please specify a boolean value like yes/no, "
"true/false or 1/0 instead.".format(val, key)
'{} is not a valid value for {} option, ' # noqa
'please specify a boolean value like yes/no, '
'true/false or 1/0 instead.'.format(val, key),
)
elif option.action == "count":
elif option.action == 'count':
with suppress(ValueError):
val = strtobool(val)
with suppress(ValueError):
val = int(val)
if not isinstance(val, int) or val < 0:
self.error(
"{} is not a valid value for {} option, " # noqa
"please instead specify either a non-negative integer "
"or a boolean value like yes/no or false/true "
"which is equivalent to 1/0.".format(val, key)
'{} is not a valid value for {} option, ' # noqa
'please instead specify either a non-negative integer '
'or a boolean value like yes/no or false/true '
'which is equivalent to 1/0.'.format(val, key),
)
elif option.action == "append":
elif option.action == 'append':
val = val.split()
val = [self.check_default(option, key, v) for v in val]
elif option.action == "callback":
elif option.action == 'callback':
assert option.callback is not None
late_eval.add(option.dest)
opt_str = option.get_opt_string()
@ -289,4 +296,4 @@ class ConfigOptionParser(CustomOptionParser):
def error(self, msg: str) -> None:
self.print_usage(sys.stderr)
self.exit(UNKNOWN_ERROR, f"{msg}\n")
self.exit(UNKNOWN_ERROR, f'{msg}\n')

View file

@ -1,27 +1,34 @@
from __future__ import annotations
import functools
import itertools
import sys
from signal import SIGINT, default_int_handler, signal
from typing import Any, Callable, Iterator, Optional, Tuple
from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar
from pip._vendor.progress.spinner import Spinner
from pip._vendor.rich.progress import (
BarColumn,
DownloadColumn,
FileSizeColumn,
Progress,
ProgressColumn,
SpinnerColumn,
TextColumn,
TimeElapsedColumn,
TimeRemainingColumn,
TransferSpeedColumn,
)
from signal import default_int_handler
from signal import SIGINT
from signal import signal
from typing import Any
from typing import Callable
from typing import Iterator
from typing import Optional
from typing import Tuple
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
from pip._internal.utils.misc import format_size
from pip._vendor.progress.bar import Bar
from pip._vendor.progress.bar import FillingCirclesBar
from pip._vendor.progress.bar import IncrementalBar
from pip._vendor.progress.spinner import Spinner
from pip._vendor.rich.progress import BarColumn
from pip._vendor.rich.progress import DownloadColumn
from pip._vendor.rich.progress import FileSizeColumn
from pip._vendor.rich.progress import Progress
from pip._vendor.rich.progress import ProgressColumn
from pip._vendor.rich.progress import SpinnerColumn
from pip._vendor.rich.progress import TextColumn
from pip._vendor.rich.progress import TimeElapsedColumn
from pip._vendor.rich.progress import TimeRemainingColumn
from pip._vendor.rich.progress import TransferSpeedColumn
try:
from pip._vendor import colorama
@ -34,7 +41,7 @@ DownloadProgressRenderer = Callable[[Iterator[bytes]], Iterator[bytes]]
def _select_progress_class(preferred: Bar, fallback: Bar) -> Bar:
encoding = getattr(preferred.file, "encoding", None)
encoding = getattr(preferred.file, 'encoding', None)
# If we don't know what encoding this file is in, then we'll just assume
# that it doesn't support unicode and use the ASCII bar.
@ -44,16 +51,16 @@ def _select_progress_class(preferred: Bar, fallback: Bar) -> Bar:
# Collect all of the possible characters we want to use with the preferred
# bar.
characters = [
getattr(preferred, "empty_fill", ""),
getattr(preferred, "fill", ""),
getattr(preferred, 'empty_fill', ''),
getattr(preferred, 'fill', ''),
]
characters += list(getattr(preferred, "phases", []))
characters += list(getattr(preferred, 'phases', []))
# Try to decode the characters we're using for the bar using the encoding
# of the given file, if this works then we'll assume that we can use the
# fancier bar and if not we'll fall back to the plaintext bar.
try:
"".join(characters).encode(encoding)
''.join(characters).encode(encoding)
except UnicodeEncodeError:
return fallback
else:
@ -126,17 +133,17 @@ class SilentBar(Bar):
class BlueEmojiBar(IncrementalBar):
suffix = "%(percent)d%%"
bar_prefix = " "
bar_suffix = " "
phases = ("\U0001F539", "\U0001F537", "\U0001F535")
suffix = '%(percent)d%%'
bar_prefix = ' '
bar_suffix = ' '
phases = ('\U0001F539', '\U0001F537', '\U0001F535')
class DownloadProgressMixin:
def __init__(self, *args: Any, **kwargs: Any) -> None:
# https://github.com/python/mypy/issues/5887
super().__init__(*args, **kwargs) # type: ignore
self.message: str = (" " * (get_indentation() + 2)) + self.message
self.message: str = (' ' * (get_indentation() + 2)) + self.message
@property
def downloaded(self) -> str:
@ -146,14 +153,14 @@ class DownloadProgressMixin:
def download_speed(self) -> str:
# Avoid zero division errors...
if self.avg == 0.0: # type: ignore
return "..."
return format_size(1 / self.avg) + "/s" # type: ignore
return '...'
return format_size(1 / self.avg) + '/s' # type: ignore
@property
def pretty_eta(self) -> str:
if self.eta: # type: ignore
return f"eta {self.eta_td}" # type: ignore
return ""
return f'eta {self.eta_td}' # type: ignore
return ''
def iter(self, it): # type: ignore
for x in it:
@ -196,8 +203,8 @@ class WindowsMixin:
class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin):
file = sys.stdout
message = "%(percent)d%%"
suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s"
message = '%(percent)d%%'
suffix = '%(downloaded)s %(download_speed)s %(pretty_eta)s'
class DefaultDownloadProgressBar(BaseDownloadProgressBar, _BaseBar):
@ -221,14 +228,14 @@ class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, BlueEmojiBar):
class DownloadProgressSpinner(
WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner
WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner,
):
file = sys.stdout
suffix = "%(downloaded)s %(download_speed)s"
suffix = '%(downloaded)s %(download_speed)s'
def next_phase(self) -> str:
if not hasattr(self, "_phaser"):
if not hasattr(self, '_phaser'):
self._phaser = itertools.cycle(self.phases)
return next(self._phaser)
@ -236,30 +243,30 @@ class DownloadProgressSpinner(
message = self.message % self
phase = self.next_phase()
suffix = self.suffix % self
line = "".join(
line = ''.join(
[
message,
" " if message else "",
' ' if message else '',
phase,
" " if suffix else "",
' ' if suffix else '',
suffix,
]
],
)
self.writeln(line)
BAR_TYPES = {
"off": (DownloadSilentBar, DownloadSilentBar),
"on": (DefaultDownloadProgressBar, DownloadProgressSpinner),
"ascii": (DownloadBar, DownloadProgressSpinner),
"pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner),
"emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner),
'off': (DownloadSilentBar, DownloadSilentBar),
'on': (DefaultDownloadProgressBar, DownloadProgressSpinner),
'ascii': (DownloadBar, DownloadProgressSpinner),
'pretty': (DownloadFillingCirclesBar, DownloadProgressSpinner),
'emoji': (DownloadBlueEmojiProgressBar, DownloadProgressSpinner),
}
def _legacy_progress_bar(
progress_bar: str, max: Optional[int]
progress_bar: str, max: int | None,
) -> DownloadProgressRenderer:
if max is None or max == 0:
return BAR_TYPES[progress_bar][1]().iter # type: ignore
@ -276,13 +283,13 @@ def _rich_progress_bar(
bar_type: str,
size: int,
) -> Iterator[bytes]:
assert bar_type == "on", "This should only be used in the default mode."
assert bar_type == 'on', 'This should only be used in the default mode.'
if not size:
total = float("inf")
columns: Tuple[ProgressColumn, ...] = (
TextColumn("[progress.description]{task.description}"),
SpinnerColumn("line", speed=1.5),
total = float('inf')
columns: tuple[ProgressColumn, ...] = (
TextColumn('[progress.description]{task.description}'),
SpinnerColumn('line', speed=1.5),
FileSizeColumn(),
TransferSpeedColumn(),
TimeElapsedColumn(),
@ -290,16 +297,16 @@ def _rich_progress_bar(
else:
total = size
columns = (
TextColumn("[progress.description]{task.description}"),
TextColumn('[progress.description]{task.description}'),
BarColumn(),
DownloadColumn(),
TransferSpeedColumn(),
TextColumn("eta"),
TextColumn('eta'),
TimeRemainingColumn(),
)
progress = Progress(*columns, refresh_per_second=30)
task_id = progress.add_task(" " * (get_indentation() + 2), total=total)
task_id = progress.add_task(' ' * (get_indentation() + 2), total=total)
with progress:
for chunk in iterable:
yield chunk
@ -307,15 +314,15 @@ def _rich_progress_bar(
def get_download_progress_renderer(
*, bar_type: str, size: Optional[int] = None
*, bar_type: str, size: int | None = None,
) -> DownloadProgressRenderer:
"""Get an object that can be used to render the download progress.
Returns a callable, that takes an iterable to "wrap".
"""
if bar_type == "on":
if bar_type == 'on':
return functools.partial(_rich_progress_bar, bar_type=bar_type, size=size)
elif bar_type == "off":
elif bar_type == 'off':
return iter # no-op, when passed an iterator
else:
return _legacy_progress_bar(bar_type, size)

View file

@ -4,42 +4,43 @@ The classes in this module are in a separate module so the commands not
needing download / PackageFinder capability don't unnecessarily import the
PackageFinder machinery and all its vendored dependencies, etc.
"""
from __future__ import annotations
import logging
import os
import sys
from functools import partial
from optparse import Values
from typing import Any, List, Optional, Tuple
from typing import Any
from typing import List
from typing import Optional
from typing import Tuple
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import Command
from pip._internal.cli.command_context import CommandContextMixIn
from pip._internal.exceptions import CommandError, PreviousBuildDirError
from pip._internal.exceptions import CommandError
from pip._internal.exceptions import PreviousBuildDirError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req.constructors import (
install_req_from_editable,
install_req_from_line,
install_req_from_parsed_requirement,
install_req_from_req_string,
)
from pip._internal.req.constructors import install_req_from_editable
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.constructors import install_req_from_parsed_requirement
from pip._internal.req.constructors import install_req_from_req_string
from pip._internal.req.req_file import parse_requirements
from pip._internal.req.req_install import InstallRequirement
from pip._internal.req.req_tracker import RequirementTracker
from pip._internal.resolution.base import BaseResolver
from pip._internal.self_outdated_check import pip_self_version_check
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.temp_dir import (
TempDirectory,
TempDirectoryTypeRegistry,
tempdir_kinds,
)
from pip._internal.utils.temp_dir import tempdir_kinds
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry
from pip._internal.utils.virtualenv import running_under_virtualenv
logger = logging.getLogger(__name__)
@ -53,17 +54,17 @@ class SessionCommandMixin(CommandContextMixIn):
def __init__(self) -> None:
super().__init__()
self._session: Optional[PipSession] = None
self._session: PipSession | None = None
@classmethod
def _get_index_urls(cls, options: Values) -> Optional[List[str]]:
def _get_index_urls(cls, options: Values) -> list[str] | None:
"""Return a list of index urls from user-provided options."""
index_urls = []
if not getattr(options, "no_index", False):
url = getattr(options, "index_url", None)
if not getattr(options, 'no_index', False):
url = getattr(options, 'index_url', None)
if url:
index_urls.append(url)
urls = getattr(options, "extra_index_urls", None)
urls = getattr(options, 'extra_index_urls', None)
if urls:
index_urls.extend(urls)
# Return None rather than an empty list
@ -82,13 +83,13 @@ class SessionCommandMixin(CommandContextMixIn):
def _build_session(
self,
options: Values,
retries: Optional[int] = None,
timeout: Optional[int] = None,
retries: int | None = None,
timeout: int | None = None,
) -> PipSession:
assert not options.cache_dir or os.path.isabs(options.cache_dir)
session = PipSession(
cache=(
os.path.join(options.cache_dir, "http") if options.cache_dir else None
os.path.join(options.cache_dir, 'http') if options.cache_dir else None
),
retries=retries if retries is not None else options.retries,
trusted_hosts=options.trusted_hosts,
@ -110,8 +111,8 @@ class SessionCommandMixin(CommandContextMixIn):
# Handle configured proxies
if options.proxy:
session.proxies = {
"http": options.proxy,
"https": options.proxy,
'http': options.proxy,
'https': options.proxy,
}
# Determine if we can prompt the user for authentication or not
@ -135,14 +136,14 @@ class IndexGroupCommand(Command, SessionCommandMixin):
This overrides the default behavior of not doing the check.
"""
# Make sure the index_group options are present.
assert hasattr(options, "no_index")
assert hasattr(options, 'no_index')
if options.disable_pip_version_check or options.no_index:
return
# Otherwise, check if we're using the latest version of pip available.
session = self._build_session(
options, retries=0, timeout=min(5, options.timeout)
options, retries=0, timeout=min(5, options.timeout),
)
with session:
pip_self_version_check(session, options)
@ -164,14 +165,14 @@ def warn_if_run_as_root() -> None:
"""
if running_under_virtualenv():
return
if not hasattr(os, "getuid"):
if not hasattr(os, 'getuid'):
return
# On Windows, there are no "system managed" Python packages. Installing as
# Administrator via pip is the correct way of updating system environments.
#
# We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform
# checks: https://mypy.readthedocs.io/en/stable/common_issues.html
if sys.platform == "win32" or sys.platform == "cygwin":
if sys.platform == 'win32' or sys.platform == 'cygwin':
return
if os.getuid() != 0:
@ -179,9 +180,9 @@ def warn_if_run_as_root() -> None:
logger.warning(
"Running pip as the 'root' user can result in broken permissions and "
"conflicting behaviour with the system package manager. "
"It is recommended to use a virtual environment instead: "
"https://pip.pypa.io/warnings/venv"
'conflicting behaviour with the system package manager. '
'It is recommended to use a virtual environment instead: '
'https://pip.pypa.io/warnings/venv',
)
@ -195,8 +196,8 @@ def with_cleanup(func: Any) -> Any:
registry.set_delete(t, False)
def wrapper(
self: RequirementCommand, options: Values, args: List[Any]
) -> Optional[int]:
self: RequirementCommand, options: Values, args: list[Any],
) -> int | None:
assert self.tempdir_registry is not None
if options.no_clean:
configure_tempdir_registry(self.tempdir_registry)
@ -222,30 +223,30 @@ class RequirementCommand(IndexGroupCommand):
@staticmethod
def determine_resolver_variant(options: Values) -> str:
"""Determines which resolver should be used, based on the given options."""
if "legacy-resolver" in options.deprecated_features_enabled:
return "legacy"
if 'legacy-resolver' in options.deprecated_features_enabled:
return 'legacy'
return "2020-resolver"
return '2020-resolver'
@staticmethod
def determine_build_failure_suppression(options: Values) -> bool:
"""Determines whether build failures should be suppressed and backtracked on."""
if "backtrack-on-build-failures" not in options.deprecated_features_enabled:
if 'backtrack-on-build-failures' not in options.deprecated_features_enabled:
return False
if "legacy-resolver" in options.deprecated_features_enabled:
raise CommandError("Cannot backtrack with legacy resolver.")
if 'legacy-resolver' in options.deprecated_features_enabled:
raise CommandError('Cannot backtrack with legacy resolver.')
deprecated(
reason=(
"Backtracking on build failures can mask issues related to how "
"a package generates metadata or builds a wheel. This flag will "
"be removed in pip 22.2."
'Backtracking on build failures can mask issues related to how '
'a package generates metadata or builds a wheel. This flag will '
'be removed in pip 22.2.'
),
gone_in=None,
replacement=(
"avoiding known-bad versions by explicitly telling pip to ignore them "
"(either directly as requirements, or via a constraints file)"
'avoiding known-bad versions by explicitly telling pip to ignore them '
'(either directly as requirements, or via a constraints file)'
),
feature_flag=None,
issue=10655,
@ -261,7 +262,7 @@ class RequirementCommand(IndexGroupCommand):
session: PipSession,
finder: PackageFinder,
use_user_site: bool,
download_dir: Optional[str] = None,
download_dir: str | None = None,
verbosity: int = 0,
) -> RequirementPreparer:
"""
@ -271,42 +272,42 @@ class RequirementCommand(IndexGroupCommand):
assert temp_build_dir_path is not None
resolver_variant = cls.determine_resolver_variant(options)
if resolver_variant == "2020-resolver":
lazy_wheel = "fast-deps" in options.features_enabled
if resolver_variant == '2020-resolver':
lazy_wheel = 'fast-deps' in options.features_enabled
if lazy_wheel:
logger.warning(
"pip is using lazily downloaded wheels using HTTP "
"range requests to obtain dependency information. "
"This experimental feature is enabled through "
"--use-feature=fast-deps and it is not ready for "
"production."
'pip is using lazily downloaded wheels using HTTP '
'range requests to obtain dependency information. '
'This experimental feature is enabled through '
'--use-feature=fast-deps and it is not ready for '
'production.',
)
else:
lazy_wheel = False
if "fast-deps" in options.features_enabled:
if 'fast-deps' in options.features_enabled:
logger.warning(
"fast-deps has no effect when used with the legacy resolver."
'fast-deps has no effect when used with the legacy resolver.',
)
in_tree_build = "out-of-tree-build" not in options.deprecated_features_enabled
if "in-tree-build" in options.features_enabled:
in_tree_build = 'out-of-tree-build' not in options.deprecated_features_enabled
if 'in-tree-build' in options.features_enabled:
deprecated(
reason="In-tree builds are now the default.",
replacement="to remove the --use-feature=in-tree-build flag",
gone_in="22.1",
reason='In-tree builds are now the default.',
replacement='to remove the --use-feature=in-tree-build flag',
gone_in='22.1',
)
if "out-of-tree-build" in options.deprecated_features_enabled:
if 'out-of-tree-build' in options.deprecated_features_enabled:
deprecated(
reason="Out-of-tree builds are deprecated.",
reason='Out-of-tree builds are deprecated.',
replacement=None,
gone_in="22.1",
gone_in='22.1',
)
if options.progress_bar not in {"on", "off"}:
if options.progress_bar not in {'on', 'off'}:
deprecated(
reason="Custom progress bar styles are deprecated",
replacement="to use the default progress bar style.",
gone_in="22.1",
reason='Custom progress bar styles are deprecated',
replacement='to use the default progress bar style.',
gone_in='22.1',
)
return RequirementPreparer(
@ -331,14 +332,14 @@ class RequirementCommand(IndexGroupCommand):
preparer: RequirementPreparer,
finder: PackageFinder,
options: Values,
wheel_cache: Optional[WheelCache] = None,
wheel_cache: WheelCache | None = None,
use_user_site: bool = False,
ignore_installed: bool = True,
ignore_requires_python: bool = False,
force_reinstall: bool = False,
upgrade_strategy: str = "to-satisfy-only",
use_pep517: Optional[bool] = None,
py_version_info: Optional[Tuple[int, ...]] = None,
upgrade_strategy: str = 'to-satisfy-only',
use_pep517: bool | None = None,
py_version_info: tuple[int, ...] | None = None,
) -> BaseResolver:
"""
Create a Resolver instance for the given parameters.
@ -353,7 +354,7 @@ class RequirementCommand(IndexGroupCommand):
# The long import name and duplicated invocation is needed to convince
# Mypy into correctly typechecking. Otherwise it would complain the
# "Resolver" class being redefined.
if resolver_variant == "2020-resolver":
if resolver_variant == '2020-resolver':
import pip._internal.resolution.resolvelib.resolver
return pip._internal.resolution.resolvelib.resolver.Resolver(
@ -388,15 +389,15 @@ class RequirementCommand(IndexGroupCommand):
def get_requirements(
self,
args: List[str],
args: list[str],
options: Values,
finder: PackageFinder,
session: PipSession,
) -> List[InstallRequirement]:
) -> list[InstallRequirement]:
"""
Parse command-line arguments into the corresponding requirements.
"""
requirements: List[InstallRequirement] = []
requirements: list[InstallRequirement] = []
for filename in options.constraints:
for parsed_req in parse_requirements(
filename,
@ -434,7 +435,7 @@ class RequirementCommand(IndexGroupCommand):
# NOTE: options.require_hashes may be set if --require-hashes is True
for filename in options.requirements:
for parsed_req in parse_requirements(
filename, finder=finder, options=options, session=session
filename, finder=finder, options=options, session=session,
):
req_to_add = install_req_from_parsed_requirement(
parsed_req,
@ -449,18 +450,18 @@ class RequirementCommand(IndexGroupCommand):
options.require_hashes = True
if not (args or options.editables or options.requirements):
opts = {"name": self.name}
opts = {'name': self.name}
if options.find_links:
raise CommandError(
"You must give at least one requirement to {name} "
'You must give at least one requirement to {name} '
'(maybe you meant "pip {name} {links}"?)'.format(
**dict(opts, links=" ".join(options.find_links))
)
**dict(opts, links=' '.join(options.find_links)),
),
)
else:
raise CommandError(
"You must give at least one requirement to {name} "
'(see "pip help {name}")'.format(**opts)
'You must give at least one requirement to {name} '
'(see "pip help {name}")'.format(**opts),
)
return requirements
@ -480,8 +481,8 @@ class RequirementCommand(IndexGroupCommand):
self,
options: Values,
session: PipSession,
target_python: Optional[TargetPython] = None,
ignore_requires_python: Optional[bool] = None,
target_python: TargetPython | None = None,
ignore_requires_python: bool | None = None,
) -> PackageFinder:
"""
Create a package finder appropriate to this requirement command.
@ -502,5 +503,5 @@ class RequirementCommand(IndexGroupCommand):
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
use_deprecated_html5lib="html5lib" in options.deprecated_features_enabled,
use_deprecated_html5lib='html5lib' in options.deprecated_features_enabled,
)

View file

@ -1,14 +1,17 @@
from __future__ import annotations
import contextlib
import itertools
import logging
import sys
import time
from typing import IO, Iterator
from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR
from typing import IO
from typing import Iterator
from pip._internal.utils.compat import WINDOWS
from pip._internal.utils.logging import get_indentation
from pip._vendor.progress import HIDE_CURSOR
from pip._vendor.progress import SHOW_CURSOR
logger = logging.getLogger(__name__)
@ -26,7 +29,7 @@ class InteractiveSpinner(SpinnerInterface):
self,
message: str,
file: IO[str] = None,
spin_chars: str = "-\\|/",
spin_chars: str = '-\\|/',
# Empirically, 8 updates/second looks nice
min_update_interval_seconds: float = 0.125,
):
@ -39,15 +42,15 @@ class InteractiveSpinner(SpinnerInterface):
self._spin_cycle = itertools.cycle(spin_chars)
self._file.write(" " * get_indentation() + self._message + " ... ")
self._file.write(' ' * get_indentation() + self._message + ' ... ')
self._width = 0
def _write(self, status: str) -> None:
assert not self._finished
# Erase what we wrote before by backspacing to the beginning, writing
# spaces to overwrite the old text, and then backspacing again
backup = "\b" * self._width
self._file.write(backup + " " * self._width + backup)
backup = '\b' * self._width
self._file.write(backup + ' ' * self._width + backup)
# Now we have a blank slate to add our status
self._file.write(status)
self._width = len(status)
@ -65,7 +68,7 @@ class InteractiveSpinner(SpinnerInterface):
if self._finished:
return
self._write(final_status)
self._file.write("\n")
self._file.write('\n')
self._file.flush()
self._finished = True
@ -79,19 +82,19 @@ class NonInteractiveSpinner(SpinnerInterface):
self._message = message
self._finished = False
self._rate_limiter = RateLimiter(min_update_interval_seconds)
self._update("started")
self._update('started')
def _update(self, status: str) -> None:
assert not self._finished
self._rate_limiter.reset()
logger.info("%s: %s", self._message, status)
logger.info('%s: %s', self._message, status)
def spin(self) -> None:
if self._finished:
return
if not self._rate_limiter.ready():
return
self._update("still running...")
self._update('still running...')
def finish(self, final_status: str) -> None:
if self._finished:
@ -129,13 +132,13 @@ def open_spinner(message: str) -> Iterator[SpinnerInterface]:
with hidden_cursor(sys.stdout):
yield spinner
except KeyboardInterrupt:
spinner.finish("canceled")
spinner.finish('canceled')
raise
except Exception:
spinner.finish("error")
spinner.finish('error')
raise
else:
spinner.finish("done")
spinner.finish('done')
@contextlib.contextmanager

View file

@ -1,3 +1,4 @@
from __future__ import annotations
SUCCESS = 0
ERROR = 1
UNKNOWN_ERROR = 2