|
import errno |
|
import json |
|
import operator |
|
import os |
|
import shutil |
|
import site |
|
from optparse import SUPPRESS_HELP, Values |
|
from typing import List, Optional |
|
|
|
from pip._vendor.rich import print_json |
|
|
|
from pip._internal.cache import WheelCache |
|
from pip._internal.cli import cmdoptions |
|
from pip._internal.cli.cmdoptions import make_target_python |
|
from pip._internal.cli.req_command import ( |
|
RequirementCommand, |
|
warn_if_run_as_root, |
|
with_cleanup, |
|
) |
|
from pip._internal.cli.status_codes import ERROR, SUCCESS |
|
from pip._internal.exceptions import CommandError, InstallationError |
|
from pip._internal.locations import get_scheme |
|
from pip._internal.metadata import get_environment |
|
from pip._internal.models.installation_report import InstallationReport |
|
from pip._internal.operations.build.build_tracker import get_build_tracker |
|
from pip._internal.operations.check import ConflictDetails, check_install_conflicts |
|
from pip._internal.req import install_given_reqs |
|
from pip._internal.req.req_install import ( |
|
InstallRequirement, |
|
check_legacy_setup_py_options, |
|
) |
|
from pip._internal.utils.compat import WINDOWS |
|
from pip._internal.utils.filesystem import test_writable_dir |
|
from pip._internal.utils.logging import getLogger |
|
from pip._internal.utils.misc import ( |
|
check_externally_managed, |
|
ensure_dir, |
|
get_pip_version, |
|
protect_pip_from_modification_on_windows, |
|
write_output, |
|
) |
|
from pip._internal.utils.temp_dir import TempDirectory |
|
from pip._internal.utils.virtualenv import ( |
|
running_under_virtualenv, |
|
virtualenv_no_global, |
|
) |
|
from pip._internal.wheel_builder import build, should_build_for_install_command |
|
|
|
logger = getLogger(__name__) |
|
|
|
|
|
class InstallCommand(RequirementCommand): |
|
""" |
|
Install packages from: |
|
|
|
- PyPI (and other indexes) using requirement specifiers. |
|
- VCS project urls. |
|
- Local project directories. |
|
- Local or remote source archives. |
|
|
|
pip also supports installing from "requirements files", which provide |
|
an easy way to specify a whole environment to be installed. |
|
""" |
|
|
|
usage = """ |
|
%prog [options] <requirement specifier> [package-index-options] ... |
|
%prog [options] -r <requirements file> [package-index-options] ... |
|
%prog [options] [-e] <vcs project url> ... |
|
%prog [options] [-e] <local project path> ... |
|
%prog [options] <archive url/path> ...""" |
|
|
|
def add_options(self) -> None: |
|
self.cmd_opts.add_option(cmdoptions.requirements()) |
|
self.cmd_opts.add_option(cmdoptions.constraints()) |
|
self.cmd_opts.add_option(cmdoptions.no_deps()) |
|
self.cmd_opts.add_option(cmdoptions.pre()) |
|
|
|
self.cmd_opts.add_option(cmdoptions.editable()) |
|
self.cmd_opts.add_option( |
|
"--dry-run", |
|
action="store_true", |
|
dest="dry_run", |
|
default=False, |
|
help=( |
|
"Don't actually install anything, just print what would be. " |
|
"Can be used in combination with --ignore-installed " |
|
"to 'resolve' the requirements." |
|
), |
|
) |
|
self.cmd_opts.add_option( |
|
"-t", |
|
"--target", |
|
dest="target_dir", |
|
metavar="dir", |
|
default=None, |
|
help=( |
|
"Install packages into <dir>. " |
|
"By default this will not replace existing files/folders in " |
|
"<dir>. Use --upgrade to replace existing packages in <dir> " |
|
"with new versions." |
|
), |
|
) |
|
cmdoptions.add_target_python_options(self.cmd_opts) |
|
|
|
self.cmd_opts.add_option( |
|
"--user", |
|
dest="use_user_site", |
|
action="store_true", |
|
help=( |
|
"Install to the Python user install directory for your " |
|
"platform. Typically ~/.local/, or %APPDATA%\\Python on " |
|
"Windows. (See the Python documentation for site.USER_BASE " |
|
"for full details.)" |
|
), |
|
) |
|
self.cmd_opts.add_option( |
|
"--no-user", |
|
dest="use_user_site", |
|
action="store_false", |
|
help=SUPPRESS_HELP, |
|
) |
|
self.cmd_opts.add_option( |
|
"--root", |
|
dest="root_path", |
|
metavar="dir", |
|
default=None, |
|
help="Install everything relative to this alternate root directory.", |
|
) |
|
self.cmd_opts.add_option( |
|
"--prefix", |
|
dest="prefix_path", |
|
metavar="dir", |
|
default=None, |
|
help=( |
|
"Installation prefix where lib, bin and other top-level " |
|
"folders are placed. Note that the resulting installation may " |
|
"contain scripts and other resources which reference the " |
|
"Python interpreter of pip, and not that of ``--prefix``. " |
|
"See also the ``--python`` option if the intention is to " |
|
"install packages into another (possibly pip-free) " |
|
"environment." |
|
), |
|
) |
|
|
|
self.cmd_opts.add_option(cmdoptions.src()) |
|
|
|
self.cmd_opts.add_option( |
|
"-U", |
|
"--upgrade", |
|
dest="upgrade", |
|
action="store_true", |
|
help=( |
|
"Upgrade all specified packages to the newest available " |
|
"version. The handling of dependencies depends on the " |
|
"upgrade-strategy used." |
|
), |
|
) |
|
|
|
self.cmd_opts.add_option( |
|
"--upgrade-strategy", |
|
dest="upgrade_strategy", |
|
default="only-if-needed", |
|
choices=["only-if-needed", "eager"], |
|
help=( |
|
"Determines how dependency upgrading should be handled " |
|
"[default: %default]. " |
|
'"eager" - dependencies are upgraded regardless of ' |
|
"whether the currently installed version satisfies the " |
|
"requirements of the upgraded package(s). " |
|
'"only-if-needed" - are upgraded only when they do not ' |
|
"satisfy the requirements of the upgraded package(s)." |
|
), |
|
) |
|
|
|
self.cmd_opts.add_option( |
|
"--force-reinstall", |
|
dest="force_reinstall", |
|
action="store_true", |
|
help="Reinstall all packages even if they are already up-to-date.", |
|
) |
|
|
|
self.cmd_opts.add_option( |
|
"-I", |
|
"--ignore-installed", |
|
dest="ignore_installed", |
|
action="store_true", |
|
help=( |
|
"Ignore the installed packages, overwriting them. " |
|
"This can break your system if the existing package " |
|
"is of a different version or was installed " |
|
"with a different package manager!" |
|
), |
|
) |
|
|
|
self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) |
|
self.cmd_opts.add_option(cmdoptions.no_build_isolation()) |
|
self.cmd_opts.add_option(cmdoptions.use_pep517()) |
|
self.cmd_opts.add_option(cmdoptions.no_use_pep517()) |
|
self.cmd_opts.add_option(cmdoptions.check_build_deps()) |
|
self.cmd_opts.add_option(cmdoptions.override_externally_managed()) |
|
|
|
self.cmd_opts.add_option(cmdoptions.config_settings()) |
|
self.cmd_opts.add_option(cmdoptions.global_options()) |
|
|
|
self.cmd_opts.add_option( |
|
"--compile", |
|
action="store_true", |
|
dest="compile", |
|
default=True, |
|
help="Compile Python source files to bytecode", |
|
) |
|
|
|
self.cmd_opts.add_option( |
|
"--no-compile", |
|
action="store_false", |
|
dest="compile", |
|
help="Do not compile Python source files to bytecode", |
|
) |
|
|
|
self.cmd_opts.add_option( |
|
"--no-warn-script-location", |
|
action="store_false", |
|
dest="warn_script_location", |
|
default=True, |
|
help="Do not warn when installing scripts outside PATH", |
|
) |
|
self.cmd_opts.add_option( |
|
"--no-warn-conflicts", |
|
action="store_false", |
|
dest="warn_about_conflicts", |
|
default=True, |
|
help="Do not warn about broken dependencies", |
|
) |
|
self.cmd_opts.add_option(cmdoptions.no_binary()) |
|
self.cmd_opts.add_option(cmdoptions.only_binary()) |
|
self.cmd_opts.add_option(cmdoptions.prefer_binary()) |
|
self.cmd_opts.add_option(cmdoptions.require_hashes()) |
|
self.cmd_opts.add_option(cmdoptions.progress_bar()) |
|
self.cmd_opts.add_option(cmdoptions.root_user_action()) |
|
|
|
index_opts = cmdoptions.make_option_group( |
|
cmdoptions.index_group, |
|
self.parser, |
|
) |
|
|
|
self.parser.insert_option_group(0, index_opts) |
|
self.parser.insert_option_group(0, self.cmd_opts) |
|
|
|
self.cmd_opts.add_option( |
|
"--report", |
|
dest="json_report_file", |
|
metavar="file", |
|
default=None, |
|
help=( |
|
"Generate a JSON file describing what pip did to install " |
|
"the provided requirements. " |
|
"Can be used in combination with --dry-run and --ignore-installed " |
|
"to 'resolve' the requirements. " |
|
"When - is used as file name it writes to stdout. " |
|
"When writing to stdout, please combine with the --quiet option " |
|
"to avoid mixing pip logging output with JSON output." |
|
), |
|
) |
|
|
|
@with_cleanup |
|
def run(self, options: Values, args: List[str]) -> int: |
|
if options.use_user_site and options.target_dir is not None: |
|
raise CommandError("Can not combine '--user' and '--target'") |
|
|
|
|
|
|
|
|
|
|
|
|
|
installing_into_current_environment = ( |
|
not (options.dry_run and options.json_report_file) |
|
and options.root_path is None |
|
and options.target_dir is None |
|
and options.prefix_path is None |
|
) |
|
if ( |
|
installing_into_current_environment |
|
and not options.override_externally_managed |
|
): |
|
check_externally_managed() |
|
|
|
upgrade_strategy = "to-satisfy-only" |
|
if options.upgrade: |
|
upgrade_strategy = options.upgrade_strategy |
|
|
|
cmdoptions.check_dist_restriction(options, check_target=True) |
|
|
|
logger.verbose("Using %s", get_pip_version()) |
|
options.use_user_site = decide_user_install( |
|
options.use_user_site, |
|
prefix_path=options.prefix_path, |
|
target_dir=options.target_dir, |
|
root_path=options.root_path, |
|
isolated_mode=options.isolated_mode, |
|
) |
|
|
|
target_temp_dir: Optional[TempDirectory] = None |
|
target_temp_dir_path: Optional[str] = None |
|
if options.target_dir: |
|
options.ignore_installed = True |
|
options.target_dir = os.path.abspath(options.target_dir) |
|
if ( |
|
|
|
os.path.exists(options.target_dir) and |
|
not os.path.isdir(options.target_dir) |
|
|
|
): |
|
raise CommandError( |
|
"Target path exists but is not a directory, will not continue." |
|
) |
|
|
|
|
|
target_temp_dir = TempDirectory(kind="target") |
|
target_temp_dir_path = target_temp_dir.path |
|
self.enter_context(target_temp_dir) |
|
|
|
global_options = options.global_options or [] |
|
|
|
session = self.get_default_session(options) |
|
|
|
target_python = make_target_python(options) |
|
finder = self._build_package_finder( |
|
options=options, |
|
session=session, |
|
target_python=target_python, |
|
ignore_requires_python=options.ignore_requires_python, |
|
) |
|
build_tracker = self.enter_context(get_build_tracker()) |
|
|
|
directory = TempDirectory( |
|
delete=not options.no_clean, |
|
kind="install", |
|
globally_managed=True, |
|
) |
|
|
|
try: |
|
reqs = self.get_requirements(args, options, finder, session) |
|
check_legacy_setup_py_options(options, reqs) |
|
|
|
wheel_cache = WheelCache(options.cache_dir) |
|
|
|
|
|
|
|
|
|
for req in reqs: |
|
req.permit_editable_wheels = True |
|
|
|
preparer = self.make_requirement_preparer( |
|
temp_build_dir=directory, |
|
options=options, |
|
build_tracker=build_tracker, |
|
session=session, |
|
finder=finder, |
|
use_user_site=options.use_user_site, |
|
verbosity=self.verbosity, |
|
) |
|
resolver = self.make_resolver( |
|
preparer=preparer, |
|
finder=finder, |
|
options=options, |
|
wheel_cache=wheel_cache, |
|
use_user_site=options.use_user_site, |
|
ignore_installed=options.ignore_installed, |
|
ignore_requires_python=options.ignore_requires_python, |
|
force_reinstall=options.force_reinstall, |
|
upgrade_strategy=upgrade_strategy, |
|
use_pep517=options.use_pep517, |
|
) |
|
|
|
self.trace_basic_info(finder) |
|
|
|
requirement_set = resolver.resolve( |
|
reqs, check_supported_wheels=not options.target_dir |
|
) |
|
|
|
if options.json_report_file: |
|
report = InstallationReport(requirement_set.requirements_to_install) |
|
if options.json_report_file == "-": |
|
print_json(data=report.to_dict()) |
|
else: |
|
with open(options.json_report_file, "w", encoding="utf-8") as f: |
|
json.dump(report.to_dict(), f, indent=2, ensure_ascii=False) |
|
|
|
if options.dry_run: |
|
|
|
|
|
requirement_set.warn_legacy_versions_and_specifiers() |
|
would_install_items = sorted( |
|
(r.metadata["name"], r.metadata["version"]) |
|
for r in requirement_set.requirements_to_install |
|
) |
|
if would_install_items: |
|
write_output( |
|
"Would install %s", |
|
" ".join("-".join(item) for item in would_install_items), |
|
) |
|
return SUCCESS |
|
|
|
try: |
|
pip_req = requirement_set.get_requirement("pip") |
|
except KeyError: |
|
modifying_pip = False |
|
else: |
|
|
|
|
|
modifying_pip = pip_req.satisfied_by is None |
|
protect_pip_from_modification_on_windows(modifying_pip=modifying_pip) |
|
|
|
reqs_to_build = [ |
|
r |
|
for r in requirement_set.requirements.values() |
|
if should_build_for_install_command(r) |
|
] |
|
|
|
_, build_failures = build( |
|
reqs_to_build, |
|
wheel_cache=wheel_cache, |
|
verify=True, |
|
build_options=[], |
|
global_options=global_options, |
|
) |
|
|
|
if build_failures: |
|
raise InstallationError( |
|
"Could not build wheels for {}, which is required to " |
|
"install pyproject.toml-based projects".format( |
|
", ".join(r.name for r in build_failures) |
|
) |
|
) |
|
|
|
to_install = resolver.get_installation_order(requirement_set) |
|
|
|
|
|
conflicts: Optional[ConflictDetails] = None |
|
should_warn_about_conflicts = ( |
|
not options.ignore_dependencies and options.warn_about_conflicts |
|
) |
|
if should_warn_about_conflicts: |
|
conflicts = self._determine_conflicts(to_install) |
|
|
|
|
|
|
|
warn_script_location = options.warn_script_location |
|
if options.target_dir or options.prefix_path: |
|
warn_script_location = False |
|
|
|
installed = install_given_reqs( |
|
to_install, |
|
global_options, |
|
root=options.root_path, |
|
home=target_temp_dir_path, |
|
prefix=options.prefix_path, |
|
warn_script_location=warn_script_location, |
|
use_user_site=options.use_user_site, |
|
pycompile=options.compile, |
|
) |
|
|
|
lib_locations = get_lib_location_guesses( |
|
user=options.use_user_site, |
|
home=target_temp_dir_path, |
|
root=options.root_path, |
|
prefix=options.prefix_path, |
|
isolated=options.isolated_mode, |
|
) |
|
env = get_environment(lib_locations) |
|
|
|
installed.sort(key=operator.attrgetter("name")) |
|
items = [] |
|
for result in installed: |
|
item = result.name |
|
try: |
|
installed_dist = env.get_distribution(item) |
|
if installed_dist is not None: |
|
item = f"{item}-{installed_dist.version}" |
|
except Exception: |
|
pass |
|
items.append(item) |
|
|
|
if conflicts is not None: |
|
self._warn_about_conflicts( |
|
conflicts, |
|
resolver_variant=self.determine_resolver_variant(options), |
|
) |
|
|
|
installed_desc = " ".join(items) |
|
if installed_desc: |
|
write_output( |
|
"Successfully installed %s", |
|
installed_desc, |
|
) |
|
except OSError as error: |
|
show_traceback = self.verbosity >= 1 |
|
|
|
message = create_os_error_message( |
|
error, |
|
show_traceback, |
|
options.use_user_site, |
|
) |
|
logger.error(message, exc_info=show_traceback) |
|
|
|
return ERROR |
|
|
|
if options.target_dir: |
|
assert target_temp_dir |
|
self._handle_target_dir( |
|
options.target_dir, target_temp_dir, options.upgrade |
|
) |
|
if options.root_user_action == "warn": |
|
warn_if_run_as_root() |
|
return SUCCESS |
|
|
|
def _handle_target_dir( |
|
self, target_dir: str, target_temp_dir: TempDirectory, upgrade: bool |
|
) -> None: |
|
ensure_dir(target_dir) |
|
|
|
|
|
|
|
lib_dir_list = [] |
|
|
|
|
|
|
|
scheme = get_scheme("", home=target_temp_dir.path) |
|
purelib_dir = scheme.purelib |
|
platlib_dir = scheme.platlib |
|
data_dir = scheme.data |
|
|
|
if os.path.exists(purelib_dir): |
|
lib_dir_list.append(purelib_dir) |
|
if os.path.exists(platlib_dir) and platlib_dir != purelib_dir: |
|
lib_dir_list.append(platlib_dir) |
|
if os.path.exists(data_dir): |
|
lib_dir_list.append(data_dir) |
|
|
|
for lib_dir in lib_dir_list: |
|
for item in os.listdir(lib_dir): |
|
if lib_dir == data_dir: |
|
ddir = os.path.join(data_dir, item) |
|
if any(s.startswith(ddir) for s in lib_dir_list[:-1]): |
|
continue |
|
target_item_dir = os.path.join(target_dir, item) |
|
if os.path.exists(target_item_dir): |
|
if not upgrade: |
|
logger.warning( |
|
"Target directory %s already exists. Specify " |
|
"--upgrade to force replacement.", |
|
target_item_dir, |
|
) |
|
continue |
|
if os.path.islink(target_item_dir): |
|
logger.warning( |
|
"Target directory %s already exists and is " |
|
"a link. pip will not automatically replace " |
|
"links, please remove if replacement is " |
|
"desired.", |
|
target_item_dir, |
|
) |
|
continue |
|
if os.path.isdir(target_item_dir): |
|
shutil.rmtree(target_item_dir) |
|
else: |
|
os.remove(target_item_dir) |
|
|
|
shutil.move(os.path.join(lib_dir, item), target_item_dir) |
|
|
|
def _determine_conflicts( |
|
self, to_install: List[InstallRequirement] |
|
) -> Optional[ConflictDetails]: |
|
try: |
|
return check_install_conflicts(to_install) |
|
except Exception: |
|
logger.exception( |
|
"Error while checking for conflicts. Please file an issue on " |
|
"pip's issue tracker: https://github.com/pypa/pip/issues/new" |
|
) |
|
return None |
|
|
|
def _warn_about_conflicts( |
|
self, conflict_details: ConflictDetails, resolver_variant: str |
|
) -> None: |
|
package_set, (missing, conflicting) = conflict_details |
|
if not missing and not conflicting: |
|
return |
|
|
|
parts: List[str] = [] |
|
if resolver_variant == "legacy": |
|
parts.append( |
|
"pip's legacy dependency resolver does not consider dependency " |
|
"conflicts when selecting packages. This behaviour is the " |
|
"source of the following dependency conflicts." |
|
) |
|
else: |
|
assert resolver_variant == "2020-resolver" |
|
parts.append( |
|
"pip's dependency resolver does not currently take into account " |
|
"all the packages that are installed. This behaviour is the " |
|
"source of the following dependency conflicts." |
|
) |
|
|
|
|
|
for project_name in missing: |
|
version = package_set[project_name][0] |
|
for dependency in missing[project_name]: |
|
message = ( |
|
"{name} {version} requires {requirement}, " |
|
"which is not installed." |
|
).format( |
|
name=project_name, |
|
version=version, |
|
requirement=dependency[1], |
|
) |
|
parts.append(message) |
|
|
|
for project_name in conflicting: |
|
version = package_set[project_name][0] |
|
for dep_name, dep_version, req in conflicting[project_name]: |
|
message = ( |
|
"{name} {version} requires {requirement}, but {you} have " |
|
"{dep_name} {dep_version} which is incompatible." |
|
).format( |
|
name=project_name, |
|
version=version, |
|
requirement=req, |
|
dep_name=dep_name, |
|
dep_version=dep_version, |
|
you=("you" if resolver_variant == "2020-resolver" else "you'll"), |
|
) |
|
parts.append(message) |
|
|
|
logger.critical("\n".join(parts)) |
|
|
|
|
|
def get_lib_location_guesses( |
|
user: bool = False, |
|
home: Optional[str] = None, |
|
root: Optional[str] = None, |
|
isolated: bool = False, |
|
prefix: Optional[str] = None, |
|
) -> List[str]: |
|
scheme = get_scheme( |
|
"", |
|
user=user, |
|
home=home, |
|
root=root, |
|
isolated=isolated, |
|
prefix=prefix, |
|
) |
|
return [scheme.purelib, scheme.platlib] |
|
|
|
|
|
def site_packages_writable(root: Optional[str], isolated: bool) -> bool: |
|
return all( |
|
test_writable_dir(d) |
|
for d in set(get_lib_location_guesses(root=root, isolated=isolated)) |
|
) |
|
|
|
|
|
def decide_user_install( |
|
use_user_site: Optional[bool], |
|
prefix_path: Optional[str] = None, |
|
target_dir: Optional[str] = None, |
|
root_path: Optional[str] = None, |
|
isolated_mode: bool = False, |
|
) -> bool: |
|
"""Determine whether to do a user install based on the input options. |
|
|
|
If use_user_site is False, no additional checks are done. |
|
If use_user_site is True, it is checked for compatibility with other |
|
options. |
|
If use_user_site is None, the default behaviour depends on the environment, |
|
which is provided by the other arguments. |
|
""" |
|
|
|
|
|
if (use_user_site is not None) and (not use_user_site): |
|
logger.debug("Non-user install by explicit request") |
|
return False |
|
|
|
if use_user_site: |
|
if prefix_path: |
|
raise CommandError( |
|
"Can not combine '--user' and '--prefix' as they imply " |
|
"different installation locations" |
|
) |
|
if virtualenv_no_global(): |
|
raise InstallationError( |
|
"Can not perform a '--user' install. User site-packages " |
|
"are not visible in this virtualenv." |
|
) |
|
logger.debug("User install by explicit request") |
|
return True |
|
|
|
|
|
assert use_user_site is None |
|
|
|
|
|
if prefix_path or target_dir: |
|
logger.debug("Non-user install due to --prefix or --target option") |
|
return False |
|
|
|
|
|
if not site.ENABLE_USER_SITE: |
|
logger.debug("Non-user install because user site-packages disabled") |
|
return False |
|
|
|
|
|
|
|
if site_packages_writable(root=root_path, isolated=isolated_mode): |
|
logger.debug("Non-user install because site-packages writeable") |
|
return False |
|
|
|
logger.info( |
|
"Defaulting to user installation because normal site-packages " |
|
"is not writeable" |
|
) |
|
return True |
|
|
|
|
|
def create_os_error_message( |
|
error: OSError, show_traceback: bool, using_user_site: bool |
|
) -> str: |
|
"""Format an error message for an OSError |
|
|
|
It may occur anytime during the execution of the install command. |
|
""" |
|
parts = [] |
|
|
|
|
|
parts.append("Could not install packages due to an OSError") |
|
if not show_traceback: |
|
parts.append(": ") |
|
parts.append(str(error)) |
|
else: |
|
parts.append(".") |
|
|
|
|
|
parts[-1] += "\n" |
|
|
|
|
|
|
|
if error.errno == errno.EACCES: |
|
user_option_part = "Consider using the `--user` option" |
|
permissions_part = "Check the permissions" |
|
|
|
if not running_under_virtualenv() and not using_user_site: |
|
parts.extend( |
|
[ |
|
user_option_part, |
|
" or ", |
|
permissions_part.lower(), |
|
] |
|
) |
|
else: |
|
parts.append(permissions_part) |
|
parts.append(".\n") |
|
|
|
|
|
|
|
if ( |
|
WINDOWS |
|
and error.errno == errno.ENOENT |
|
and error.filename |
|
and len(error.filename) > 260 |
|
): |
|
parts.append( |
|
"HINT: This error might have occurred since " |
|
"this system does not have Windows Long Path " |
|
"support enabled. You can find information on " |
|
"how to enable this at " |
|
"https://pip.pypa.io/warnings/enable-long-paths\n" |
|
) |
|
|
|
return "".join(parts).strip() + "\n" |
|
|