# Copyright (C) 2012 Anaconda, Inc
# SPDX-License-Identifier: BSD-3-Clause
"""
Functions related to core conda functionality that relates to pip
NOTE: This modules used to in conda, as conda/pip.py
"""
import json
import os
import re
import sys
from logging import getLogger
from conda.base.context import context
from conda.deprecations import deprecated
from conda.exceptions import CondaEnvException
from conda.exports import on_win
from conda.gateways.subprocess import any_subprocess
log = getLogger(__name__)
def pip_subprocess(args, prefix, cwd):
if on_win:
python_path = os.path.join(prefix, "python.exe")
else:
python_path = os.path.join(prefix, "bin", "python")
run_args = [python_path, "-m", "pip"] + args
stdout, stderr, rc = any_subprocess(run_args, prefix, cwd=cwd)
if not context.quiet and not context.json:
print("Ran pip subprocess with arguments:")
print(run_args)
print("Pip subprocess output:")
print(stdout)
if rc != 0:
print("Pip subprocess error:", file=sys.stderr)
print(stderr, file=sys.stderr)
raise CondaEnvException("Pip failed")
# This will modify (break) Context. We have a context stack but need to verify it works
# stdout, stderr, rc = run_command(Commands.RUN, *run_args, stdout=None, stderr=None)
return stdout, stderr
def get_pip_installed_packages(stdout):
"""Return the list of pip packages installed based on the command output"""
m = re.search(r"Successfully installed\ (.*)", stdout)
if m:
return m.group(1).strip().split()
else:
return None
@deprecated("23.9", "24.3")
def get_pip_version(prefix):
stdout, stderr = pip_subprocess(["-V"], prefix)
pip_version = re.search(r"pip\ (\d+\.\d+\.\d+)", stdout)
if not pip_version:
raise CondaEnvException("Failed to find pip version string in output")
else:
pip_version = pip_version.group(1)
return pip_version
@deprecated("23.9", "24.3")
class PipPackage(dict):
def __str__(self):
if "path" in self:
return "{} ({})-{}-<pip>".format(
self["name"], self["path"], self["version"]
)
return "{}-{}-<pip>".format(self["name"], self["version"])
@deprecated("23.9", "24.3")
def installed(prefix, output=True):
pip_version = get_pip_version(prefix)
pip_major_version = int(pip_version.split(".", 1)[0])
env = os.environ.copy()
args = ["list"]
if pip_major_version >= 9:
args += ["--format", "json"]
else:
env["PIP_FORMAT"] = "legacy"
try:
pip_stdout, stderr = pip_subprocess(args, prefix=prefix, env=env)
except Exception:
# Any error should just be ignored
if output:
print("# Warning: subprocess call to pip failed", file=sys.stderr)
return
if pip_major_version >= 9:
pkgs = json.loads(pip_stdout)
# For every package in pipinst that is not already represented
# in installed append a fake name to installed with 'pip'
# as the build string
for kwargs in pkgs:
kwargs["name"] = kwargs["name"].lower()
if ", " in kwargs["version"]:
# Packages installed with setup.py develop will include a path in
# the version. They should be included here, even if they are
# installed with conda, as they are preferred over the conda
# version. We still include the conda version, though, because it
# is still installed.
version, path = kwargs["version"].split(", ", 1)
# We do this because the code below uses rsplit('-', 2)
version = version.replace("-", " ")
kwargs["version"] = version
kwargs["path"] = path
yield PipPackage(**kwargs)
else:
# For every package in pipinst that is not already represented
# in installed append a fake name to installed with 'pip'
# as the build string
pat = re.compile(r"([\w.-]+)\s+\((.+)\)")
for line in pip_stdout.splitlines():
line = line.strip()
if not line:
continue
m = pat.match(line)
if m is None:
if output:
print(
"Could not extract name and version from: %r" % line,
file=sys.stderr,
)
continue
name, version = m.groups()
name = name.lower()
kwargs = {
"name": name,
"version": version,
}
if ", " in version:
# Packages installed with setup.py develop will include a path in
# the version. They should be included here, even if they are
# installed with conda, as they are preferred over the conda
# version. We still include the conda version, though, because it
# is still installed.
version, path = version.split(", ")
# We do this because the code below uses rsplit('-', 2)
version = version.replace("-", " ")
kwargs.update(
{
"path": path,
"version": version,
}
)
yield PipPackage(**kwargs)
# canonicalize_{regex,name} inherited from packaging/utils.py
# Used under BSD license
_canonicalize_regex = re.compile(r"[-_.]+")
@deprecated("23.9", "24.3")
def _canonicalize_name(name):
# This is taken from PEP 503.
return _canonicalize_regex.sub("-", name).lower()
@deprecated("23.9", "24.3")
def add_pip_installed(prefix, installed_pkgs, json=None, output=True):
# Defer to json for backwards compatibility
if isinstance(json, bool):
output = not json
# TODO Refactor so installed is a real list of objects/dicts
# instead of strings allowing for direct comparison
# split :: to get rid of channel info
# canonicalize names for pip comparison
# because pip normalizes `foo_bar` to `foo-bar`
conda_names = {_canonicalize_name(rec.name) for rec in installed_pkgs}
for pip_pkg in installed(prefix, output=output):
pip_name = _canonicalize_name(pip_pkg["name"])
if pip_name in conda_names and "path" not in pip_pkg:
continue
installed_pkgs.add(str(pip_pkg))