from __future__ import annotations
import os
import shutil
import subprocess
from argparse import ArgumentParser
from subprocess import CompletedProcess
from typing import Callable, List, Tuple
from termcolor import colored
from typing_extensions import TypedDict, Unpack
[docs]
class SubprocessKwarg(TypedDict, total=False):
stdout: int
stderr: int
cwd: str
[docs]
class Run:
PIPE = subprocess.PIPE
def __init__(self, verbose: bool = False, colorize: bool = False) -> None:
self.setup(verbose, colorize)
[docs]
def setup(self, verbose: bool = False, colorize: bool = False) -> None:
self.verbose = verbose
self.colorize = colorize
def _print_cmd(self, cmd: List[str]) -> None:
if self.colorize:
output: List[str] = []
for arg in cmd:
if arg.startswith("--"):
output.append(colored(arg, color="yellow"))
elif arg.startswith("-"):
output.append(colored(arg, color="blue"))
elif os.path.exists(arg):
output.append(colored(arg, color="white", on_color="on_cyan"))
else:
output.append(arg)
print(" ".join(output))
else:
print(" ".join(cmd))
[docs]
def run(
self, cmd: List[str], **kwargs: Unpack[SubprocessKwarg]
) -> CompletedProcess[str]:
if self.verbose:
self._print_cmd(cmd)
return subprocess.run(cmd, encoding="utf-8", **kwargs)
[docs]
def check_output(self, cmd: List[str], **kwargs: Unpack[SubprocessKwarg]) -> bytes:
if self.verbose:
self._print_cmd(cmd)
return subprocess.check_output(cmd, **kwargs)
[docs]
def check_dependencies(
*executables: Tuple[str, str] | str, raise_error: bool = True
) -> bool:
"""Check if the given executables are existing in $PATH.
:param executables: A tuple of executables to check for their
existence in $PATH. Each element of the tuple can be either a string
(e. g. `pdfimages`) or a itself a tuple `('pdfimages', 'poppler')`.
The first entry of this tuple is the name of the executable the second
entry is a description text which is displayed in the raised exception.
:param raise_error: Raise an error if an executable doesn’t exist.
:return: True if all executables exist. False if one or
more executables not exist.
"""
errors: List[str] = []
for executable in executables:
if isinstance(executable, tuple):
if not shutil.which(executable[0]):
errors.append("{} ({})".format(executable[0], executable[1]))
else:
if not shutil.which(executable):
errors.append(executable)
if errors:
if raise_error:
raise SystemError("Some commands are not installed: " + ", ".join(errors))
else:
return False
else:
return True
[docs]
class FilePath:
absolute: bool
"""Boolean value indicating whether the path is an absolute or an
relative path."""
filename: str
"""The filename is the combination of the basename and the
extension, e. g. `file.ext`."""
extension: str
"""The extension of the file, e. g. `ext`."""
basename: str
"""The basename of the file, e. g. `file`."""
base: str
"""The path without an extension, e. g. `/home/document/file`."""
def __init__(self, path: str, absolute: bool = False):
self.absolute = absolute
if self.absolute:
self.path = os.path.abspath(path)
else:
self.path = os.path.relpath(path)
self.filename = os.path.basename(path)
self.extension = os.path.splitext(self.path)[1][1:]
self.basename = self.filename[: -len(self.extension) - 1]
self.base = self.path[: -len(self.extension) - 1]
def __str__(self):
return self.path
def __eq__(self, other: object) -> bool:
return self.path == other.path
def _export(self, path: str) -> FilePath:
return FilePath(path, self.absolute)
[docs]
def new(
self, extension: str | None = None, append: str = "", del_substring: str = ""
) -> FilePath:
"""
:param extension: The extension of the new file path.
:param append: String to append on the basename. This string
is located before the extension.
:param del_substring: String to delete from the new file path.
:return: A new file path object.
"""
if not extension:
extension = self.extension
new = "{}{}.{}".format(self.base, append, extension)
if del_substring:
new = new.replace(del_substring, "")
return self._export(new)
[docs]
def remove(self) -> None:
"""Remove the file."""
os.remove(self.path)
[docs]
def argparser_to_readme(
argparser: Callable[[], ArgumentParser],
template: str = "README-template.md",
destination: str = "README.md",
indentation: int = 0,
placeholder: str = "{{ argparse }}",
) -> None:
"""Add the formatted help output of a command line utility using the
Python module `argparse` to a README file. Make sure to set the name
of the program (`prop`) or you get strange program names.
:param argparser: A function that returns an object.
:param template: The path of a template text file containing the
placeholder. Default: `README-template.md`
:param destination: The path of the destination file. Default:
`README.me`
:param indentation: Indent the formatted help output by X spaces.
Default: 0
:param placeholder: Placeholder string that gets replaced by the
formatted help output. Default: `{{ argparse }}`
"""
help_string = argparser().format_help()
if indentation > 0:
indent_lines: List[str] = []
lines = help_string.split("\n")
for line in lines:
indent_lines.append(" " * indentation + line)
help_string = "\n".join(indent_lines)
with open(template, "r", encoding="utf-8") as template_file:
template_string = template_file.read()
readme = template_string.replace(placeholder, help_string)
readme_file = open(destination, "w")
readme_file.write(readme)
readme_file.close()