2026-06-02 15:36:25 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
class Result:
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
lib.Result: Initialize with status = None
Define default parameter values for Result's constructor, namely None
for exit status, stdout and stderr.
Instantiating a Result object without parameters signifies "this
object doesn't contain data from a real process's exit event". Up to
now, similar meaning has been hand-crafted by ExecContext's run() and
friends by using an error exit status (1) to make sure it wasn't
mistaken for success. This commit formalizes that into the Result
structure itself, but uses None instead for the exit status.
Controlling default values in Result itself also means that the
Result class gets better awareness of what it contains, and its log
messages and stdin / stdout can be more fitting:
- If a real process failed, make stdout return at least b''
- If a real process succeeded, make stdout return at least b''
Returning something from .stdout on success fixes a real bug: An
attempt to access what "rpm -U somepackage.rpm" returns, namely
nothing, raises a bogus exception, because stdout is None.
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-06-15 07:30:17 +02:00
|
|
|
stdout: bytes | None = None,
|
|
|
|
|
stderr: bytes | None = None,
|
|
|
|
|
status: int | None = None, # Command has not yet exited
|
2026-06-02 15:36:25 +02:00
|
|
|
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
|
|
|
|
|
|
2026-06-13 04:43:34 +02:00
|
|
|
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)
|
|
|
|
|
|
2026-06-02 15:36:25 +02:00
|
|
|
@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:
|
lib.Result: Initialize with status = None
Define default parameter values for Result's constructor, namely None
for exit status, stdout and stderr.
Instantiating a Result object without parameters signifies "this
object doesn't contain data from a real process's exit event". Up to
now, similar meaning has been hand-crafted by ExecContext's run() and
friends by using an error exit status (1) to make sure it wasn't
mistaken for success. This commit formalizes that into the Result
structure itself, but uses None instead for the exit status.
Controlling default values in Result itself also means that the
Result class gets better awareness of what it contains, and its log
messages and stdin / stdout can be more fitting:
- If a real process failed, make stdout return at least b''
- If a real process succeeded, make stdout return at least b''
Returning something from .stdout on success fixes a real bug: An
attempt to access what "rpm -U somepackage.rpm" returns, namely
nothing, raises a bogus exception, because stdout is None.
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-06-15 07:30:17 +02:00
|
|
|
if self.__status == 0:
|
|
|
|
|
return b''
|
2026-06-02 15:36:25 +02:00
|
|
|
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:
|
lib.Result: Initialize with status = None
Define default parameter values for Result's constructor, namely None
for exit status, stdout and stderr.
Instantiating a Result object without parameters signifies "this
object doesn't contain data from a real process's exit event". Up to
now, similar meaning has been hand-crafted by ExecContext's run() and
friends by using an error exit status (1) to make sure it wasn't
mistaken for success. This commit formalizes that into the Result
structure itself, but uses None instead for the exit status.
Controlling default values in Result itself also means that the
Result class gets better awareness of what it contains, and its log
messages and stdin / stdout can be more fitting:
- If a real process failed, make stdout return at least b''
- If a real process succeeded, make stdout return at least b''
Returning something from .stdout on success fixes a real bug: An
attempt to access what "rpm -U somepackage.rpm" returns, namely
nothing, raises a bogus exception, because stdout is None.
Signed-off-by: Jan Lindemann <jan@janware.com>
2026-06-15 07:30:17 +02:00
|
|
|
if isinstance(self.__status, int) and self.__status != 0:
|
|
|
|
|
return b''
|
2026-06-02 15:36:25 +02:00
|
|
|
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)
|