updates
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
A thin `subprocess` wrapper for making long-running subprocesses more
|
||||
responsive from the `pip-audit` CLI.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import subprocess
|
||||
from collections.abc import Sequence
|
||||
from subprocess import Popen
|
||||
|
||||
from ._state import AuditState
|
||||
|
||||
|
||||
class CalledProcessError(Exception):
|
||||
"""
|
||||
Raised if the underlying subprocess created by `run` exits with a nonzero code.
|
||||
"""
|
||||
|
||||
def __init__(self, msg: str, *, stderr: str) -> None:
|
||||
"""
|
||||
Create a new `CalledProcessError`.
|
||||
"""
|
||||
super().__init__(msg)
|
||||
self.stderr = stderr
|
||||
|
||||
|
||||
def run(args: Sequence[str], *, log_stdout: bool = False, state: AuditState = AuditState()) -> str:
|
||||
"""
|
||||
Execute the given arguments.
|
||||
|
||||
Uses `state` to provide feedback on the subprocess's status.
|
||||
|
||||
Raises a `CalledProcessError` if the subprocess fails. Otherwise, returns
|
||||
the process's `stdout` stream as a string.
|
||||
"""
|
||||
|
||||
# NOTE(ww): We frequently run commands inside of ephemeral virtual environments,
|
||||
# which have long absolute paths on some platforms. These make for confusing
|
||||
# state updates, so we trim the first argument down to its basename.
|
||||
pretty_args = " ".join([os.path.basename(args[0]), *args[1:]])
|
||||
|
||||
terminated = False
|
||||
stdout = b""
|
||||
stderr = b""
|
||||
|
||||
# Run the process with unbuffered I/O, to make the poll-and-read loop below
|
||||
# more responsive.
|
||||
with Popen(args, bufsize=0, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as process:
|
||||
# NOTE: We use `poll()` to control this loop instead of the `read()` call
|
||||
# to prevent deadlocks. Similarly, `read(size)` will return an empty bytes
|
||||
# once `stdout` hits EOF, so we don't have to worry about that blocking.
|
||||
while not terminated:
|
||||
terminated = process.poll() is not None
|
||||
stdout += process.stdout.read() # type: ignore
|
||||
stderr += process.stderr.read() # type: ignore
|
||||
state.update_state(
|
||||
f"Running {pretty_args}",
|
||||
stdout.decode(errors="replace") if log_stdout else None,
|
||||
)
|
||||
|
||||
if process.returncode != 0:
|
||||
raise CalledProcessError(
|
||||
f"{pretty_args} exited with {process.returncode}",
|
||||
stderr=stderr.decode(errors="replace"),
|
||||
)
|
||||
|
||||
return stdout.decode("utf-8", errors="replace")
|
||||
Reference in New Issue
Block a user