morbin.morbin

  1import argparse
  2import shlex
  3import subprocess
  4from contextlib import contextmanager
  5from dataclasses import dataclass
  6
  7from pathier import Pathier
  8from typing_extensions import Self
  9
 10root = Pathier(__file__).parent
 11
 12
 13@dataclass
 14class Output:
 15    """Dataclass representing the output of a terminal command.
 16
 17    #### Fields:
 18    * `return_code: list[int]`
 19    * `stdout: str`
 20    * `stderr: str`"""
 21
 22    return_code: list[int]
 23    stdout: str = ""
 24    stderr: str = ""
 25
 26    def __add__(self, output: Self) -> Self:
 27        return self.__class__(
 28            self.return_code + output.return_code,
 29            self.stdout + output.stdout,
 30            self.stderr + output.stderr,
 31        )
 32
 33
 34class Morbin:
 35    """Base class for creating python bindings for cli programs.
 36
 37    At a minimum, any subclass must implement a `program` property that returns the name used to invoke the cli.
 38
 39    The `run` function can then be used to build bindings.
 40
 41    >>> class Pip(Morbin):
 42    >>>     @property
 43    >>>     def program(self)->str:
 44    >>>         return 'pip'
 45    >>>
 46    >>>     def install(self, package:str, *args:str)->Output:
 47    >>>         return self.run("install", package, *args)
 48    >>>
 49    >>>     def upgrade(self, package:str)->Output:
 50    >>>         return self.install(package, "--upgrade")
 51    >>>
 52    >>>     def install_requirements(self)->Output:
 53    >>>         return self.install("-r", "requirements.txt")
 54    >>>
 55    >>> pip = Pip()
 56    >>> pip.upgrade("morbin")"""
 57
 58    def __init__(self, capture_output: bool = False, shell: bool = False):
 59        """Command bindings should return an `Output` object.
 60
 61        If `capture_output` is `True` or the `capturing_output` context manager is used,
 62        the command's output will be available via `Output.stdout` and `Output.stderr`.
 63
 64        This property can be used to parse and use the command output or to simply execute commands "silently".
 65
 66        The return code will also be available via `Output.return_code`.
 67
 68        If `shell` is `True`, commands will be executed in the system shell (necessary on Windows for builtin shell commands like `cd` and `dir`).
 69
 70        [Security concerns using shell = True](https://docs.python.org/3/library/subprocess.html#security-considerations)
 71
 72        """
 73        self.capture_output = capture_output
 74        self.shell = shell
 75
 76    @property
 77    def capture_output(self) -> bool:
 78        """If `True`, member functions will return the generated `stdout` as a string,
 79        otherwise they return the command's exit code as a string (so my type checker doesn't throw a fit about ints.).
 80        """
 81        return self._capture_output
 82
 83    @capture_output.setter
 84    def capture_output(self, should_capture: bool):
 85        self._capture_output = should_capture
 86
 87    @property
 88    def program(self) -> str:
 89        """The name used to invoke the program from the command line."""
 90        raise NotImplementedError
 91
 92    @property
 93    def shell(self) -> bool:
 94        """If `True`, commands will be executed in the system shell."""
 95        return self._shell
 96
 97    @shell.setter
 98    def shell(self, should_use: bool):
 99        self._shell = should_use
100
101    @contextmanager
102    def capturing_output(self):
103        """Ensures `self.capture_output` is `True` while within the context.
104
105        Upon exiting the context, `self.capture_output` will be set back to whatever it was when the context was entered.
106        """
107        original_state = self.capture_output
108        self.capture_output = True
109        yield self
110        self.capture_output = original_state
111
112    def run(self, *args: str) -> Output:
113        """Run this program with any number of args.
114
115        Returns an `Output` object."""
116        command = [self.program]
117        for arg in args:
118            command.extend(shlex.split(arg))
119        if self.capture_output:
120            output = subprocess.run(
121                command,
122                stdout=subprocess.PIPE,
123                stderr=subprocess.PIPE,
124                text=True,
125                shell=self.shell,
126            )
127            return Output([output.returncode], output.stdout, output.stderr)
128        else:
129            output = subprocess.run(command, shell=self.shell)
130            return Output([output.returncode])
131
132
133def get_args() -> argparse.Namespace:
134    parser = argparse.ArgumentParser()
135    parser.add_argument(
136        "name",
137        type=str,
138        help=" The program name to create a template subclass of Morbin for. ",
139    )
140    args = parser.parse_args()
141    return args
142
143
144def main(args: argparse.Namespace | None = None):
145    if not args:
146        args = get_args()
147    template = (root / "template.py").read_text()
148    template = template.replace("Name", args.name.capitalize()).replace(
149        "name", args.name
150    )
151    (Pathier.cwd() / f"{args.name}.py").write_text(template)
152
153
154if __name__ == "__main__":
155    main(get_args())
@dataclass
class Output:
14@dataclass
15class Output:
16    """Dataclass representing the output of a terminal command.
17
18    #### Fields:
19    * `return_code: list[int]`
20    * `stdout: str`
21    * `stderr: str`"""
22
23    return_code: list[int]
24    stdout: str = ""
25    stderr: str = ""
26
27    def __add__(self, output: Self) -> Self:
28        return self.__class__(
29            self.return_code + output.return_code,
30            self.stdout + output.stdout,
31            self.stderr + output.stderr,
32        )

Dataclass representing the output of a terminal command.

Fields:

  • return_code: list[int]
  • stdout: str
  • stderr: str
Output(return_code: list[int], stdout: str = '', stderr: str = '')
class Morbin:
 35class Morbin:
 36    """Base class for creating python bindings for cli programs.
 37
 38    At a minimum, any subclass must implement a `program` property that returns the name used to invoke the cli.
 39
 40    The `run` function can then be used to build bindings.
 41
 42    >>> class Pip(Morbin):
 43    >>>     @property
 44    >>>     def program(self)->str:
 45    >>>         return 'pip'
 46    >>>
 47    >>>     def install(self, package:str, *args:str)->Output:
 48    >>>         return self.run("install", package, *args)
 49    >>>
 50    >>>     def upgrade(self, package:str)->Output:
 51    >>>         return self.install(package, "--upgrade")
 52    >>>
 53    >>>     def install_requirements(self)->Output:
 54    >>>         return self.install("-r", "requirements.txt")
 55    >>>
 56    >>> pip = Pip()
 57    >>> pip.upgrade("morbin")"""
 58
 59    def __init__(self, capture_output: bool = False, shell: bool = False):
 60        """Command bindings should return an `Output` object.
 61
 62        If `capture_output` is `True` or the `capturing_output` context manager is used,
 63        the command's output will be available via `Output.stdout` and `Output.stderr`.
 64
 65        This property can be used to parse and use the command output or to simply execute commands "silently".
 66
 67        The return code will also be available via `Output.return_code`.
 68
 69        If `shell` is `True`, commands will be executed in the system shell (necessary on Windows for builtin shell commands like `cd` and `dir`).
 70
 71        [Security concerns using shell = True](https://docs.python.org/3/library/subprocess.html#security-considerations)
 72
 73        """
 74        self.capture_output = capture_output
 75        self.shell = shell
 76
 77    @property
 78    def capture_output(self) -> bool:
 79        """If `True`, member functions will return the generated `stdout` as a string,
 80        otherwise they return the command's exit code as a string (so my type checker doesn't throw a fit about ints.).
 81        """
 82        return self._capture_output
 83
 84    @capture_output.setter
 85    def capture_output(self, should_capture: bool):
 86        self._capture_output = should_capture
 87
 88    @property
 89    def program(self) -> str:
 90        """The name used to invoke the program from the command line."""
 91        raise NotImplementedError
 92
 93    @property
 94    def shell(self) -> bool:
 95        """If `True`, commands will be executed in the system shell."""
 96        return self._shell
 97
 98    @shell.setter
 99    def shell(self, should_use: bool):
100        self._shell = should_use
101
102    @contextmanager
103    def capturing_output(self):
104        """Ensures `self.capture_output` is `True` while within the context.
105
106        Upon exiting the context, `self.capture_output` will be set back to whatever it was when the context was entered.
107        """
108        original_state = self.capture_output
109        self.capture_output = True
110        yield self
111        self.capture_output = original_state
112
113    def run(self, *args: str) -> Output:
114        """Run this program with any number of args.
115
116        Returns an `Output` object."""
117        command = [self.program]
118        for arg in args:
119            command.extend(shlex.split(arg))
120        if self.capture_output:
121            output = subprocess.run(
122                command,
123                stdout=subprocess.PIPE,
124                stderr=subprocess.PIPE,
125                text=True,
126                shell=self.shell,
127            )
128            return Output([output.returncode], output.stdout, output.stderr)
129        else:
130            output = subprocess.run(command, shell=self.shell)
131            return Output([output.returncode])

Base class for creating python bindings for cli programs.

At a minimum, any subclass must implement a program property that returns the name used to invoke the cli.

The run function can then be used to build bindings.

>>> class Pip(Morbin):
>>>     @property
>>>     def program(self)->str:
>>>         return 'pip'
>>>
>>>     def install(self, package:str, *args:str)->Output:
>>>         return self.run("install", package, *args)
>>>
>>>     def upgrade(self, package:str)->Output:
>>>         return self.install(package, "--upgrade")
>>>
>>>     def install_requirements(self)->Output:
>>>         return self.install("-r", "requirements.txt")
>>>
>>> pip = Pip()
>>> pip.upgrade("morbin")
Morbin(capture_output: bool = False, shell: bool = False)
59    def __init__(self, capture_output: bool = False, shell: bool = False):
60        """Command bindings should return an `Output` object.
61
62        If `capture_output` is `True` or the `capturing_output` context manager is used,
63        the command's output will be available via `Output.stdout` and `Output.stderr`.
64
65        This property can be used to parse and use the command output or to simply execute commands "silently".
66
67        The return code will also be available via `Output.return_code`.
68
69        If `shell` is `True`, commands will be executed in the system shell (necessary on Windows for builtin shell commands like `cd` and `dir`).
70
71        [Security concerns using shell = True](https://docs.python.org/3/library/subprocess.html#security-considerations)
72
73        """
74        self.capture_output = capture_output
75        self.shell = shell

Command bindings should return an Output object.

If capture_output is True or the capturing_output context manager is used, the command's output will be available via Output.stdout and Output.stderr.

This property can be used to parse and use the command output or to simply execute commands "silently".

The return code will also be available via Output.return_code.

If shell is True, commands will be executed in the system shell (necessary on Windows for builtin shell commands like cd and dir).

Security concerns using shell = True

capture_output: bool

If True, member functions will return the generated stdout as a string, otherwise they return the command's exit code as a string (so my type checker doesn't throw a fit about ints.).

shell: bool

If True, commands will be executed in the system shell.

program: str

The name used to invoke the program from the command line.

@contextmanager
def capturing_output(self):
102    @contextmanager
103    def capturing_output(self):
104        """Ensures `self.capture_output` is `True` while within the context.
105
106        Upon exiting the context, `self.capture_output` will be set back to whatever it was when the context was entered.
107        """
108        original_state = self.capture_output
109        self.capture_output = True
110        yield self
111        self.capture_output = original_state

Ensures self.capture_output is True while within the context.

Upon exiting the context, self.capture_output will be set back to whatever it was when the context was entered.

def run(self, *args: str) -> morbin.morbin.Output:
113    def run(self, *args: str) -> Output:
114        """Run this program with any number of args.
115
116        Returns an `Output` object."""
117        command = [self.program]
118        for arg in args:
119            command.extend(shlex.split(arg))
120        if self.capture_output:
121            output = subprocess.run(
122                command,
123                stdout=subprocess.PIPE,
124                stderr=subprocess.PIPE,
125                text=True,
126                shell=self.shell,
127            )
128            return Output([output.returncode], output.stdout, output.stderr)
129        else:
130            output = subprocess.run(command, shell=self.shell)
131            return Output([output.returncode])

Run this program with any number of args.

Returns an Output object.

def get_args() -> argparse.Namespace:
134def get_args() -> argparse.Namespace:
135    parser = argparse.ArgumentParser()
136    parser.add_argument(
137        "name",
138        type=str,
139        help=" The program name to create a template subclass of Morbin for. ",
140    )
141    args = parser.parse_args()
142    return args
def main(args: argparse.Namespace | None = None):
145def main(args: argparse.Namespace | None = None):
146    if not args:
147        args = get_args()
148    template = (root / "template.py").read_text()
149    template = template.replace("Name", args.name.capitalize()).replace(
150        "name", args.name
151    )
152    (Pathier.cwd() / f"{args.name}.py").write_text(template)