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