jw-pkg/src/python/jw/pkg/lib/Result.py
Jan Lindemann 048726a1aa
lib.Result: Fill "cmd" ctor parameter

Commands executed by ExecContext and its derived classes don't populate the "cmd" parameter of "Result"'s constructor. Fixing that makes for nicer error messages.

Signed-off-by: Jan Lindemann <jan@janware.com>
2026-06-17 17:12:42 +02:00

179 lines
5 KiB
Python

from __future__ import annotations
class Result:
def __init__(
self,
stdout: bytes | None = None,
stderr: bytes | None = None,
status: int | None = None, # Command has not yet exited
encoding: str = 'UTF-8',
strip: bool = True,
cmd: list[str] | None = None,
wd: str | None = None,
) -> None:
self.__stdout = stdout
self.__stderr = stderr
self.__status = status
self.__encoding = encoding
self.__strip = strip
self.__cmd = cmd
self.__wd = wd
def __decode(self, stdxxx: bytes | None) -> str | None:
if stdxxx is None:
return None
ret = stdxxx.decode(self.encoding)
if self.strip:
return ret.strip()
return ret
def __try_decode(
self,
stdxxx: bytes | None,
quote = False,
truncate: int | None = None,
annotate: bool = True,
label: str | None = None,
) -> str:
if label is None:
label = ''
else:
label = f'{label}: '
if stdxxx is None:
return f'{label}None'
try:
ret = stdxxx.decode()[:truncate].strip()
except UnicodeDecodeError:
chunk = stdxxx[:truncate]
ret = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in chunk)
if (not annotate) or truncate is None or len(stdxxx) <= truncate:
return f'{label}"{ret}"' if quote else label + ret
ret = '{labe}"{ret} ..."' if quote else '{labe}{ret} ...'
ret += f' + {len(stdxxx) - truncate} more bytes'
return ret
def __summarize(
self,
cmd: list[str] | None = None,
wd: str | None = None,
verbose = True
) -> str:
if not verbose:
ret = f'{self.__status}: '
else:
from .util import pretty_cmd
if cmd is None:
cmd = self.__cmd
call = ''
if cmd is not None:
if wd is None:
wd = self.__wd
call = f'"{pretty_cmd(cmd, wd)}" '
ret = (
f'Command {call}has not yet exited' if self.__status is None else
f'Command {call}has exited with status {self.__status}'
)
label, stdxxx, truncate = (
('stdout', self.__stdout, 40) if self.status == 0
else ('stderr', self.__stderr, None)
)
ret += self.__try_decode(
stdxxx, quote = True, truncate = truncate, annotate = True, label = label
)
return ret
def __repr__(self) -> str:
return self.__summarize(verbose = False)
@property
def status(self) -> int | None:
return self.__status
@property
def encoding(self) -> str:
return self.__encoding
@encoding.setter
def encoding(self, value: str) -> None:
self.__encoding = value
@property
def strip(self) -> bool:
return self.__strip
@strip.setter
def strip(self, value: bool) -> None:
self.__strip = value
@property
def cmd(self) -> list[str] | None:
return self.__cmd
@cmd.setter
def cmd(self, value: list[str]) -> None:
self.__cmd = value
@property
def wd(self) -> str | None:
return self.__wd
@wd.setter
def wd(self, value: str) -> None:
self.__wd = value
def matches_error(self, pattern: str) -> bool:
if self.status == 0:
return False
err = self.stderr_str
if err is None:
return False
import re
return re.search(pattern, err) is not None
def summarize(self, cmd: list[str] | None = None, wd: str | None = None) -> str:
return self.__summarize(cmd, wd)
@property
def summary(self) -> str:
return self.__summarize(None, None)
@property
def stdout(self) -> bytes:
if self.__stdout is None:
if self.__status == 0:
return b''
raise Exception(f'Result has no standard output stream: {self.summary}')
return self.__stdout
@property
def stdout_or_none(self) -> bytes | None:
return self.__stdout
@property
def stdout_str_or_none(self) -> str | None:
return self.__decode(self.__stdout)
@property
def stdout_str(self) -> str:
return self.stdout.decode(self.__encoding)
@property
def stderr(self) -> bytes:
if self.__stderr is None:
if isinstance(self.__status, int) and self.__status != 0:
return b''
raise Exception(f'Result has no standard error stream: {self.summary}')
return self.__stderr
@property
def stderr_or_none(self) -> bytes | None:
return self.__stderr
@property
def stderr_str_or_none(self) -> str | None:
return self.__decode(self.__stderr)
@property
def stderr_str(self) -> str:
return self.stderr.decode(self.__encoding)