240 lines
7.4 KiB
Python
240 lines
7.4 KiB
Python
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
from typing import Literal
|
|
import logging
|
|
|
|
from email.parser import BytesHeaderParser
|
|
from typing import List, Union
|
|
|
|
|
|
json_format: Literal["json"] = "json"
|
|
string_format: Literal["string"] = "string"
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
json_format = "json"
|
|
string_format = "string"
|
|
|
|
|
|
class PipProcessError(Exception):
|
|
action: str
|
|
package: str
|
|
stderr: str = ""
|
|
stdout: str = ""
|
|
returncode: int = 1
|
|
|
|
def __init__(
|
|
self,
|
|
action: str,
|
|
package: str,
|
|
stderr: str = "",
|
|
stdout: str = "",
|
|
returncode: int = 1,
|
|
):
|
|
self.action = action
|
|
self.package = package
|
|
self.stderr = stderr
|
|
self.stdout = stdout
|
|
self.returncode = returncode
|
|
message = (
|
|
f"PipProcessError: {action} on '{package}' failed with"
|
|
"return code {returncode}.\n"
|
|
f"Stdout:\n{stdout}\n"
|
|
f"Stderr:\n{stderr}"
|
|
)
|
|
super().__init__(message)
|
|
|
|
|
|
def pip_process_with_custom_exception(
|
|
action: str,
|
|
package: str = "",
|
|
flags: List[str] = [],
|
|
format: Union[Literal["string"], Literal["json"]] = string_format,
|
|
quiet: bool = False,
|
|
no_color: bool = False,
|
|
) -> Union[str, dict]:
|
|
try:
|
|
if not quiet:
|
|
logger.debug(f"running pip {action} {' '.join(flags)} {package}")
|
|
command = [sys.executable, "-m", "pip", action]
|
|
command.extend(flags)
|
|
if package:
|
|
command.append(package)
|
|
|
|
env = dict(os.environ)
|
|
if no_color:
|
|
env["NO_COLOR"] = "true"
|
|
|
|
result = subprocess.run(
|
|
command,
|
|
env=env,
|
|
capture_output=True, # Capture both stdout and stderr
|
|
text=True, # Automatically decode to strings
|
|
check=True, # Automatically raise error on non-zero exit code
|
|
)
|
|
|
|
if format == json_format:
|
|
try:
|
|
remove_color_codes = re.compile(r"\x1b\[[0-9;]*m")
|
|
parsed_as_string = re.sub(remove_color_codes, "", result.stdout.strip())
|
|
return json.loads(parsed_as_string)
|
|
except Exception:
|
|
logger.debug(
|
|
f"JSON parse exception in decoding output from pip {action}"
|
|
f" {package}. Falling back to accumulating the byte stream",
|
|
)
|
|
accumulator = {}
|
|
parsed = BytesHeaderParser().parsebytes(result.stdout.encode())
|
|
for key, value in parsed.items():
|
|
accumulator[key] = value
|
|
return accumulator
|
|
|
|
return result.stdout
|
|
|
|
except subprocess.CalledProcessError as exc:
|
|
raise PipProcessError(action, package, exc.stderr, exc.stdout, exc.returncode)
|
|
except Exception as e:
|
|
raise PipProcessError(action, package, stderr=str(e), stdout="", returncode=1)
|
|
|
|
|
|
def installer_process(
|
|
action: str,
|
|
package: str = "",
|
|
flags: List[str] = [],
|
|
format: Union[Literal["string"], Literal["json"]] = string_format,
|
|
quiet: bool = False,
|
|
no_color: bool = False,
|
|
installer: str = "pip",
|
|
) -> Union[str, dict]:
|
|
"""Run a package install action using the specified installer (uv or pip).
|
|
|
|
Args:
|
|
action: The pip action to run (e.g., "install").
|
|
package: The package name to act on.
|
|
flags: Additional flags to pass to the installer.
|
|
format: Output format ("string" or "json").
|
|
quiet: Whether to suppress output.
|
|
no_color: Whether to disable color output.
|
|
installer: Package installer to use ("uv" or "pip").
|
|
"""
|
|
try:
|
|
if installer == "uv":
|
|
command = ["uv", "pip", action]
|
|
else:
|
|
command = [sys.executable, "-m", "pip", action]
|
|
|
|
if not quiet:
|
|
installer_label = "uv pip" if installer == "uv" else "pip"
|
|
logger.debug(
|
|
f"running {installer_label} {action} {' '.join(flags)} {package}"
|
|
)
|
|
|
|
command.extend(flags)
|
|
if package:
|
|
command.append(package)
|
|
|
|
env = dict(os.environ)
|
|
if no_color:
|
|
env["NO_COLOR"] = "true"
|
|
|
|
result = subprocess.run(
|
|
command,
|
|
env=env,
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
|
|
if format == json_format:
|
|
try:
|
|
remove_color_codes = re.compile(r"\x1b\[[0-9;]*m")
|
|
parsed_as_string = re.sub(remove_color_codes, "", result.stdout.strip())
|
|
return json.loads(parsed_as_string)
|
|
except Exception:
|
|
logger.debug(
|
|
f"JSON parse exception in decoding output from {action}"
|
|
f" {package}. Falling back to accumulating the byte stream",
|
|
)
|
|
accumulator = {}
|
|
parsed = BytesHeaderParser().parsebytes(result.stdout.encode())
|
|
for key, value in parsed.items():
|
|
accumulator[key] = value
|
|
return accumulator
|
|
|
|
return result.stdout
|
|
|
|
except subprocess.CalledProcessError as exc:
|
|
raise PipProcessError(
|
|
action, package, exc.stderr or "", exc.stdout or "", exc.returncode
|
|
)
|
|
except Exception as e:
|
|
raise PipProcessError(action, package, stderr=str(e), stdout="", returncode=1)
|
|
|
|
|
|
def pip_process(
|
|
action: str,
|
|
package: str = "",
|
|
flags: List[str] = [],
|
|
format: Union[Literal["string"], Literal["json"]] = string_format,
|
|
quiet: bool = False,
|
|
no_color: bool = False,
|
|
) -> Union[str, dict]:
|
|
try:
|
|
if not quiet:
|
|
logger.debug(f"running pip {action} {' '.join(flags)} {package}")
|
|
command = [sys.executable, "-m", "pip", action]
|
|
command.extend(flags)
|
|
if package:
|
|
command.append(package)
|
|
|
|
env = dict(os.environ)
|
|
if no_color:
|
|
env["NO_COLOR"] = "true"
|
|
|
|
result = subprocess.run(
|
|
command,
|
|
env=env,
|
|
capture_output=True, # Capture both stdout and stderr
|
|
text=True, # Automatically decode to strings
|
|
check=True, # Automatically raise error on non-zero exit code
|
|
)
|
|
|
|
if format == json_format:
|
|
try:
|
|
remove_color_codes = re.compile(r"\x1b\[[0-9;]*m")
|
|
parsed_as_string = re.sub(remove_color_codes, "", result.stdout.strip())
|
|
return json.loads(parsed_as_string)
|
|
except Exception:
|
|
logger.debug(
|
|
f"JSON parse exception in decoding output from pip {action}"
|
|
f" {package}. Falling back to accumulating the byte stream",
|
|
)
|
|
accumulator = {}
|
|
parsed = BytesHeaderParser().parsebytes(result.stdout.encode())
|
|
for key, value in parsed.items():
|
|
accumulator[key] = value
|
|
return accumulator
|
|
|
|
return result.stdout
|
|
|
|
except subprocess.CalledProcessError as exc:
|
|
logger.error(
|
|
(
|
|
f"Failed to {action} {package}\n"
|
|
f"Exit code: {exc.returncode}\n"
|
|
f"stderr: {(exc.stderr or '').strip()}\n"
|
|
f"stdout: {(exc.stdout or '').strip()}"
|
|
)
|
|
)
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
logger.error(
|
|
f"An unexpected exception occurred while trying to {action} {package}!",
|
|
e,
|
|
)
|
|
sys.exit(1)
|