Extract to module podman_compose_tools with enhanced Command classes
parent
e486a3c506
commit
505248a495
@ -0,0 +1,17 @@
|
|||||||
|
from .command import (
|
||||||
|
Command,
|
||||||
|
ArgCommand,
|
||||||
|
ShellCommand,
|
||||||
|
)
|
||||||
|
from .completed import (
|
||||||
|
CompletedExec,
|
||||||
|
)
|
||||||
|
from .execution import (
|
||||||
|
ExecutorTarget,
|
||||||
|
)
|
||||||
|
from expander import (
|
||||||
|
BinaryExecutor,
|
||||||
|
)
|
||||||
|
from .host import (
|
||||||
|
HostExecutor,
|
||||||
|
)
|
@ -0,0 +1,13 @@
|
|||||||
|
from typing import Iterable, List, NewType, Optional
|
||||||
|
|
||||||
|
|
||||||
|
CommandArgs = NewType("CommandArgs", List[str])
|
||||||
|
ShellCommandStr = NewType("ShellCommandStr", str)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_cmds(command: Iterable[Optional[str]]) -> CommandArgs:
|
||||||
|
return CommandArgs([arg for arg in command if arg is not None])
|
||||||
|
|
||||||
|
|
||||||
|
def combine_cmds(*commands: CommandArgs | List[Optional[str]]) -> CommandArgs:
|
||||||
|
return CommandArgs([arg for cmd in commands for arg in filter_cmds(cmd)])
|
@ -0,0 +1,159 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import abc
|
||||||
|
from pathlib import PurePath
|
||||||
|
import shlex
|
||||||
|
from typing import Callable, Iterable, List, Optional
|
||||||
|
|
||||||
|
from attrs import define
|
||||||
|
|
||||||
|
from .base import CommandArgs, ShellCommandStr
|
||||||
|
from .completed import CompletedExec
|
||||||
|
from .execution import ExecutorTarget
|
||||||
|
|
||||||
|
|
||||||
|
class Command(metaclass=abc.ABCMeta):
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
executor: ExecutorTarget,
|
||||||
|
check: bool = True,
|
||||||
|
capture_stdout: bool = True,
|
||||||
|
) -> CompletedExec:
|
||||||
|
return self.run(
|
||||||
|
executor=executor,
|
||||||
|
check=check,
|
||||||
|
capture_stdout=capture_stdout,
|
||||||
|
)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
executor: ExecutorTarget,
|
||||||
|
check: bool = True,
|
||||||
|
capture_stdout: bool = True,
|
||||||
|
) -> CompletedExec:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class ArgCommand(Command):
|
||||||
|
args: List[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_single(cls, arg: str) -> ArgCommand:
|
||||||
|
return cls(args=[arg])
|
||||||
|
|
||||||
|
def __add__(
|
||||||
|
self,
|
||||||
|
other: ArgCommand | CommandArgs | Iterable[Optional[str]],
|
||||||
|
) -> ArgCommand:
|
||||||
|
if isinstance(other, ArgCommand):
|
||||||
|
return ArgCommand(args=self.args + other.args)
|
||||||
|
return ArgCommand(args=self.args + [arg for arg in other if arg is not None])
|
||||||
|
|
||||||
|
def __radd__(
|
||||||
|
self,
|
||||||
|
other: ArgCommand | CommandArgs | Iterable[Optional[str]],
|
||||||
|
) -> ArgCommand:
|
||||||
|
if isinstance(other, ArgCommand):
|
||||||
|
return ArgCommand(args=other.args + self.args)
|
||||||
|
return ArgCommand(args=[arg for arg in other if arg is not None] + self.args)
|
||||||
|
|
||||||
|
def __and__(
|
||||||
|
self,
|
||||||
|
other: Optional[str],
|
||||||
|
) -> ArgCommand:
|
||||||
|
if other is None:
|
||||||
|
return self
|
||||||
|
return self + [other]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return " ".join(shlex.quote(arg) for arg in self.args)
|
||||||
|
|
||||||
|
def to_shell_cmd(self) -> ShellCommand:
|
||||||
|
return ShellCommand(command=str(self))
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
executor: ExecutorTarget,
|
||||||
|
check: bool = True,
|
||||||
|
capture_stdout: bool = True,
|
||||||
|
work_dir: Optional[PurePath] = None,
|
||||||
|
) -> CompletedExec:
|
||||||
|
return executor.exec_cmd(
|
||||||
|
command=CommandArgs(self.args),
|
||||||
|
check=check,
|
||||||
|
capture_stdout=capture_stdout,
|
||||||
|
work_dir=work_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@define(order=False)
|
||||||
|
class ShellCommand(Command):
|
||||||
|
command: str
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_other(
|
||||||
|
method: Callable[[ShellCommand, str], str]
|
||||||
|
) -> Callable[[ShellCommand, Command], ShellCommand]:
|
||||||
|
def decorated(self, other: Command) -> ShellCommand:
|
||||||
|
other_cmd: str
|
||||||
|
if isinstance(other, Command):
|
||||||
|
other_cmd = str(other)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
return ShellCommand(method(self, other_cmd))
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_path(
|
||||||
|
method: Callable[[ShellCommand, str], str]
|
||||||
|
) -> Callable[[ShellCommand, PurePath | str], ShellCommand]:
|
||||||
|
def decorated(self, other: PurePath | str) -> ShellCommand:
|
||||||
|
other_cmd: str
|
||||||
|
if isinstance(other, str):
|
||||||
|
other_cmd = other
|
||||||
|
elif isinstance(other, PurePath):
|
||||||
|
other_cmd = str(other)
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
return ShellCommand(method(self, other_cmd))
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
@_extract_other
|
||||||
|
def __or__(self, other_cmd: str) -> str: # |
|
||||||
|
return f"({self.command}) | ({other_cmd})"
|
||||||
|
|
||||||
|
@_extract_other
|
||||||
|
def __ror__(self, other_cmd: str) -> str: # |
|
||||||
|
return f"({other_cmd}) | ({self.command})"
|
||||||
|
|
||||||
|
@_parse_path
|
||||||
|
def __lt__(self, other: str) -> str: # <
|
||||||
|
return f"{self.command} < {shlex.quote(other)}"
|
||||||
|
|
||||||
|
@_parse_path
|
||||||
|
def __gt__(self, other: str) -> str: # >
|
||||||
|
return f"{self.command} > {shlex.quote(other)}"
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.command
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
executor: ExecutorTarget,
|
||||||
|
check: bool = True,
|
||||||
|
capture_stdout: bool = True,
|
||||||
|
work_dir: Optional[PurePath] = None,
|
||||||
|
) -> CompletedExec:
|
||||||
|
return executor.exec_shell(
|
||||||
|
shell_cmd=ShellCommandStr(self.command),
|
||||||
|
check=check,
|
||||||
|
capture_stdout=capture_stdout,
|
||||||
|
work_dir=work_dir,
|
||||||
|
)
|
@ -0,0 +1,20 @@
|
|||||||
|
import json
|
||||||
|
from subprocess import CompletedProcess
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from attrs import define
|
||||||
|
|
||||||
|
|
||||||
|
@define()
|
||||||
|
class CompletedExec:
|
||||||
|
completed_process: CompletedProcess
|
||||||
|
|
||||||
|
@property
|
||||||
|
def returncode(self) -> int:
|
||||||
|
return self.completed_process.returncode
|
||||||
|
|
||||||
|
def check_returncode(self) -> None:
|
||||||
|
return self.completed_process.check_returncode()
|
||||||
|
|
||||||
|
def to_json(self) -> Mapping[str, Any]:
|
||||||
|
return json.loads(self.completed_process.stdout)
|
@ -0,0 +1,76 @@
|
|||||||
|
import abc
|
||||||
|
from functools import cached_property
|
||||||
|
from pathlib import PurePath
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from attrs import define
|
||||||
|
|
||||||
|
from .base import CommandArgs, ShellCommandStr
|
||||||
|
from .completed import CompletedExec
|
||||||
|
|
||||||
|
|
||||||
|
# List of POSIX shells which shall be used
|
||||||
|
DETECTED_SHELLS = [
|
||||||
|
"/usr/bin/bash",
|
||||||
|
"/bin/bash",
|
||||||
|
"/usr/bin/sh",
|
||||||
|
"/bin/sh",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ExecutorTarget(metaclass=abc.ABCMeta):
|
||||||
|
@abc.abstractmethod
|
||||||
|
def exec_cmd(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
command: CommandArgs,
|
||||||
|
check: bool,
|
||||||
|
capture_stdout: bool,
|
||||||
|
work_dir: Optional[PurePath],
|
||||||
|
) -> CompletedExec:
|
||||||
|
...
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def process_tester(
|
||||||
|
exec: Callable[[CommandArgs], CompletedExec]
|
||||||
|
) -> Callable[[CommandArgs], bool]:
|
||||||
|
return lambda command: exec(command).returncode == 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _search_shell_with(tester: Callable[[CommandArgs], bool]) -> str:
|
||||||
|
for shell in DETECTED_SHELLS:
|
||||||
|
command = CommandArgs([shell, "-c", "true"])
|
||||||
|
if tester(command):
|
||||||
|
return command[0]
|
||||||
|
# TODO specialize
|
||||||
|
raise Exception(
|
||||||
|
f"Could not find an acceptable shell on this host, searched for {DETECTED_SHELLS}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def found_shell(self) -> str:
|
||||||
|
return self._search_shell_with(
|
||||||
|
self.process_tester(
|
||||||
|
lambda command: self.exec_cmd(
|
||||||
|
command=command,
|
||||||
|
check=False,
|
||||||
|
capture_stdout=False,
|
||||||
|
work_dir=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def exec_shell(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
shell_cmd: ShellCommandStr,
|
||||||
|
check: bool,
|
||||||
|
capture_stdout: bool,
|
||||||
|
work_dir: Optional[PurePath],
|
||||||
|
) -> CompletedExec:
|
||||||
|
return self.exec_cmd(
|
||||||
|
command=CommandArgs([self.found_shell, "-c", shell_cmd]),
|
||||||
|
check=check,
|
||||||
|
capture_stdout=capture_stdout,
|
||||||
|
work_dir=work_dir,
|
||||||
|
)
|
@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import PurePath
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from attrs import define
|
||||||
|
|
||||||
|
from .base import CommandArgs
|
||||||
|
from .command import ArgCommand
|
||||||
|
from .completed import CompletedExec
|
||||||
|
from .execution import ExecutorTarget
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class BinaryExecutor(ExecutorTarget):
|
||||||
|
binary_args: CommandArgs
|
||||||
|
|
||||||
|
def exec_cmd(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
command: CommandArgs,
|
||||||
|
check: bool,
|
||||||
|
capture_stdout: bool,
|
||||||
|
work_dir: Optional[PurePath],
|
||||||
|
) -> CompletedExec:
|
||||||
|
return super().exec_cmd(
|
||||||
|
command=CommandArgs(self.binary_args + command),
|
||||||
|
check=check,
|
||||||
|
capture_stdout=capture_stdout,
|
||||||
|
work_dir=work_dir,
|
||||||
|
)
|
@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import PurePath
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .base import CommandArgs
|
||||||
|
from .completed import CompletedExec
|
||||||
|
from .execution import ExecutorTarget
|
||||||
|
from ..misc.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
class HostExecutor(ExecutorTarget, metaclass=Singleton):
|
||||||
|
def exec_cmd(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
command: CommandArgs,
|
||||||
|
check: bool,
|
||||||
|
capture_stdout: bool,
|
||||||
|
work_dir: Optional[PurePath] = None,
|
||||||
|
) -> CompletedExec:
|
||||||
|
return CompletedExec(
|
||||||
|
subprocess.run(
|
||||||
|
args=command,
|
||||||
|
check=check,
|
||||||
|
cwd=work_dir,
|
||||||
|
shell=False,
|
||||||
|
stdout=subprocess.PIPE if capture_stdout else None,
|
||||||
|
)
|
||||||
|
)
|
@ -0,0 +1 @@
|
|||||||
|
from .singleton import Singleton
|
@ -0,0 +1,18 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Type, TypeVar
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="Singleton")
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton(type):
|
||||||
|
_instances = dict[Type, Any]()
|
||||||
|
|
||||||
|
def __call__(cls: T, *args, **kwargs) -> T:
|
||||||
|
if cls not in cls._instances:
|
||||||
|
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
|
||||||
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Singleton"]
|
Loading…
Reference in New Issue