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
|
|
|
|
|
|
|
|
|
|
@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
|
|
|
|
|
|
|
|
|
|
def __stdout_footprint(self, quote = False) -> str:
|
|
|
|
|
if self.__stdout is None:
|
|
|
|
|
ret = ''
|
|
|
|
|
else:
|
2026-06-10 12:26:01 +02:00
|
|
|
max_len = 40
|
|
|
|
|
try:
|
|
|
|
|
ret = self.stdout_str[:max_len]
|
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
|
chunk = self.__stdout[:max_len]
|
|
|
|
|
ret = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in chunk)
|
2026-06-02 15:36:25 +02:00
|
|
|
if quote:
|
2026-06-10 11:33:43 +02:00
|
|
|
ret = f'"{ret}"'
|
2026-06-02 15:36:25 +02:00
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
|
|
ret = f'{self.__status}:'
|
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 is not None:
|
|
|
|
|
if self.status != 0:
|
|
|
|
|
ret += f' err: {self.stderr_str_or_none}'
|
|
|
|
|
else:
|
|
|
|
|
ret += f' out: {self.__stdout_footprint(quote=True)}'
|
2026-06-02 15:36:25 +02:00
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
@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, wd: str | None = None) -> str:
|
|
|
|
|
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)}" '
|
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
|
|
|
ret = (
|
|
|
|
|
f'Command {call}has not yet exited' if self.__status is None else
|
|
|
|
|
f'Command {call}has exited with status {self.__status}'
|
|
|
|
|
)
|
2026-06-02 15:36:25 +02:00
|
|
|
call = pretty_cmd(cmd, wd)
|
|
|
|
|
if self.status != 0:
|
|
|
|
|
ret += f' -> stderr="{self.__stderr!r}"'
|
|
|
|
|
else:
|
|
|
|
|
if self.__stdout:
|
|
|
|
|
ret += f' -> stdout has {len(self.__stdout)} bytes'
|
|
|
|
|
else:
|
|
|
|
|
ret += ' -> stdout = None'
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
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)
|