lib.ExecContext: Support bytes-typed cmd_input

The Input instance passed as cmd_input to ExecContext.run() and
.sudo() currently may be of type str. Allow to pass bytes, too.

At the same time, disallow None to be passed as cmd_input. Force the
caller to be more explicit how it wants input to be handled, notably
with respect to interactivity.

Along the way fix a bug: Content in cmd_input should result in
CallContext.interactive == False but doesn't. Fix that.

Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-04-15 14:02:44 +02:00
commit 04b294917f
9 changed files with 46 additions and 29 deletions

View file

@ -18,7 +18,7 @@ class InputMode(Enum):
OptInteractive = auto()
Auto = auto()
Input: TypeAlias = InputMode | None | str
Input: TypeAlias = InputMode | bytes | str
class Result(NamedTuple):
@ -61,19 +61,27 @@ class ExecContext(abc.ABC):
# -- At the end of this dance, interactive needs to be either True
# or False
interactive: bool|None = None
match cmd_input:
case InputMode.Interactive:
interactive = True
case InputMode.NonInteractive:
interactive = False
case InputMode.OptInteractive:
if not isinstance(cmd_input, InputMode):
interactive = False
self.__cmd_input = (
cmd_input if isinstance(cmd_input, bytes) else
cmd_input.encode(sys.stdout.encoding or "utf-8")
)
else:
match cmd_input:
case InputMode.Interactive:
interactive = True
case InputMode.NonInteractive:
interactive = False
case InputMode.OptInteractive:
interactive = parent.interactive
case InputMode.Auto:
interactive = sys.stdin.isatty()
if interactive is None:
interactive = parent.interactive
case InputMode.Auto:
if interactive is None:
interactive = sys.stdin.isatty()
if interactive is None:
interactive = parent.interactive
if interactive is None:
interactive = sys.stdin.isatty()
self.__cmd_input = None
assert interactive in [ True, False ]
self.__interactive = interactive
@ -106,7 +114,7 @@ class ExecContext(abc.ABC):
return self.__verbose
@property
def cmd_input(self) -> bool:
def cmd_input(self) -> bytes|None:
return self.__cmd_input
@property
@ -211,6 +219,10 @@ class ExecContext(abc.ABC):
In PTY mode stderr is always None because PTY merges stdout/stderr.
"""
# Note that in the calls to the wrapped method, cmd_input == None can
# be returned by CallContext and is very much allowed
assert cmd_input is not None
ret = Result(None, None, 1)
with self.CallContext(self, title=title, cmd=cmd, cmd_input=cmd_input, wd=wd,
log_prefix='|', throw=throw, verbose=verbose) as cc:
@ -246,6 +258,10 @@ class ExecContext(abc.ABC):
title: str=None,
) -> Result:
# Note that in the calls to the wrapped method, cmd_input == None can
# be returned by CallContext and is very much allowed
assert cmd_input is not None
ret = Result(None, None, 1)
if opts is None:
opts = {}