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())
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
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")
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
).
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.).
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.
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.
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)