edq.clilib.model
This will look for objects that "look like" CLI tools. A package looks like a CLI package if it has __main__.py and __init__.py files. A module looks like a CLI tool if it has the following functions:
def _get_parser() -> argparse.ArgumentParser:def run_cli(args: argparse.Namespace) -> int:
CLI packages should always have a __main__.py file, even if they only contain other packages.
1""" 2This will look for objects that "look like" CLI tools. 3A package looks like a CLI package if it has __main__.py and __init__.py files. 4A module looks like a CLI tool if it has the following functions: 5 - `def _get_parser() -> argparse.ArgumentParser:` 6 - `def run_cli(args: argparse.Namespace) -> int:` 7 8CLI packages should always have a __main__.py file, 9even if they only contain other packages. 10""" 11 12import abc 13import argparse 14import io 15import os 16import typing 17 18import edq.util.dirent 19import edq.util.pyimport 20 21class CLIDirent(abc.ABC): 22 """ A dirent that looks like it is related to a CLI. """ 23 24 def __init__(self, 25 path: str, 26 qualified_name: str, 27 pymodule: typing.Any, 28 ) -> None: 29 self.path: str = path 30 """ 31 The path for the given dirent. 32 For a package, this will point to `__init__.py`. 33 """ 34 35 self.qualified_name: str = qualified_name 36 """ The Python qualified path to this object. """ 37 38 self.pymodule: typing.Any = pymodule 39 """ The loaded module for the given path. """ 40 41 def base_name(self) -> str: 42 """ Get the base (unqualified) name for this dirent. """ 43 44 return self.qualified_name.split('.')[-1] 45 46 @abc.abstractmethod 47 def get_description(self) -> str: 48 """ Get the description of this dirent. """ 49 50 @staticmethod 51 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIDirent', None]: 52 """ Load a representation of the CLI path (or None if the path is not a CLI dirent). """ 53 54 path = os.path.abspath(path) 55 56 if (not os.path.exists(path)): 57 return None 58 59 base_name = os.path.basename(path) 60 if (base_name.startswith('__')): 61 return None 62 63 if (os.path.isfile(path)): 64 return CLIModule.from_path(path, qualified_name = qualified_name) 65 66 return CLIPackage.from_path(path, qualified_name = qualified_name) 67 68class CLIPackage(CLIDirent): 69 """ 70 A CLI package. 71 Must have a `__main__.py` file. 72 """ 73 74 def __init__(self, 75 path: str, 76 qualified_name: str, 77 pymodule: typing.Any, 78 dirents: typing.Union[typing.List[CLIDirent], None] = None, 79 ) -> None: 80 super().__init__(path, qualified_name, pymodule) 81 82 if (dirents is None): 83 dirents = [] 84 85 self.dirents: typing.List[CLIDirent] = dirents 86 """ Entries within this package. """ 87 88 def get_description(self) -> str: 89 if (self.pymodule.__doc__ is None): 90 return '' 91 92 return self.pymodule.__doc__.strip() 93 94 @staticmethod 95 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIPackage', None]: 96 """ Load a representation of the CLI package (or None if the path is not a CLI dirent). """ 97 98 path = os.path.abspath(path) 99 100 if (not os.path.isdir(path)): 101 raise ValueError(f"CLI package path does not point to a dir: '{path}'.") 102 103 main_path = os.path.join(path, '__main__.py') 104 if (not os.path.exists(main_path)): 105 return None 106 107 init_path = os.path.join(path, '__init__.py') 108 if (not os.path.exists(init_path)): 109 return None 110 111 try: 112 init_module = edq.util.pyimport.import_path(init_path) 113 except Exception as ex: 114 raise ValueError(f"Failed to import module __init__.py file: '{init_path}'.") from ex 115 116 package = CLIPackage(path, qualified_name, init_module) 117 118 for dirent in sorted(os.listdir(path)): 119 dirent_path = os.path.join(path, dirent) 120 121 dirent_qualified_name = os.path.splitext(dirent)[0] 122 if (qualified_name != '.'): 123 dirent_qualified_name = qualified_name + '.' + dirent_qualified_name 124 125 cli_dirent = CLIDirent.from_path(dirent_path, dirent_qualified_name) 126 if (cli_dirent is not None): 127 package.dirents.append(cli_dirent) 128 129 return package 130 131class CLIModule(CLIDirent): 132 """ 133 A CLI module. 134 Must have the following functions: 135 - `def _get_parser() -> argparse.ArgumentParser:` 136 - `def run_cli(args: argparse.Namespace) -> int:` 137 """ 138 139 def __init__(self, 140 path: str, 141 qualified_name: str, 142 pymodule: typing.Any, 143 parser: argparse.ArgumentParser, 144 ) -> None: 145 super().__init__(path, qualified_name, pymodule) 146 147 self.parser: argparse.ArgumentParser = parser 148 """ The argument parser for this CLI module. """ 149 150 def get_description(self) -> str: 151 if (self.parser.description is None): 152 return '' 153 154 return self.parser.description 155 156 def get_help_text(self) -> str: 157 """ Get the help text from the parser. """ 158 159 buffer = io.StringIO() 160 self.parser.print_help(file = buffer) 161 text = buffer.getvalue() 162 buffer.close() 163 164 return text 165 166 def get_usage_text(self) -> str: 167 """ Get the help text from the parser. """ 168 169 buffer = io.StringIO() 170 self.parser.print_usage(file = buffer) 171 text = buffer.getvalue() 172 buffer.close() 173 174 return text 175 176 @staticmethod 177 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIModule', None]: 178 """ Load a representation of the CLI module (or None if the path is not a CLI dirent). """ 179 180 path = os.path.abspath(path) 181 182 if (not path.endswith('.py')): 183 return None 184 185 if (not os.path.isfile(path)): 186 raise ValueError(f"CLI module path does not point to a file: '{path}'.") 187 188 try: 189 module = edq.util.pyimport.import_path(path) 190 except Exception as ex: 191 raise ValueError(f"Failed to import module: '{path}'.") from ex 192 193 # Check if this looks like a CLI module. 194 if ('_get_parser' not in dir(module)): 195 return None 196 197 parser = module._get_parser() 198 parser.prog = 'python3 -m ' + qualified_name 199 200 return CLIModule(path, qualified_name, module, parser)
22class CLIDirent(abc.ABC): 23 """ A dirent that looks like it is related to a CLI. """ 24 25 def __init__(self, 26 path: str, 27 qualified_name: str, 28 pymodule: typing.Any, 29 ) -> None: 30 self.path: str = path 31 """ 32 The path for the given dirent. 33 For a package, this will point to `__init__.py`. 34 """ 35 36 self.qualified_name: str = qualified_name 37 """ The Python qualified path to this object. """ 38 39 self.pymodule: typing.Any = pymodule 40 """ The loaded module for the given path. """ 41 42 def base_name(self) -> str: 43 """ Get the base (unqualified) name for this dirent. """ 44 45 return self.qualified_name.split('.')[-1] 46 47 @abc.abstractmethod 48 def get_description(self) -> str: 49 """ Get the description of this dirent. """ 50 51 @staticmethod 52 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIDirent', None]: 53 """ Load a representation of the CLI path (or None if the path is not a CLI dirent). """ 54 55 path = os.path.abspath(path) 56 57 if (not os.path.exists(path)): 58 return None 59 60 base_name = os.path.basename(path) 61 if (base_name.startswith('__')): 62 return None 63 64 if (os.path.isfile(path)): 65 return CLIModule.from_path(path, qualified_name = qualified_name) 66 67 return CLIPackage.from_path(path, qualified_name = qualified_name)
A dirent that looks like it is related to a CLI.
42 def base_name(self) -> str: 43 """ Get the base (unqualified) name for this dirent. """ 44 45 return self.qualified_name.split('.')[-1]
Get the base (unqualified) name for this dirent.
47 @abc.abstractmethod 48 def get_description(self) -> str: 49 """ Get the description of this dirent. """
Get the description of this dirent.
51 @staticmethod 52 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIDirent', None]: 53 """ Load a representation of the CLI path (or None if the path is not a CLI dirent). """ 54 55 path = os.path.abspath(path) 56 57 if (not os.path.exists(path)): 58 return None 59 60 base_name = os.path.basename(path) 61 if (base_name.startswith('__')): 62 return None 63 64 if (os.path.isfile(path)): 65 return CLIModule.from_path(path, qualified_name = qualified_name) 66 67 return CLIPackage.from_path(path, qualified_name = qualified_name)
Load a representation of the CLI path (or None if the path is not a CLI dirent).
69class CLIPackage(CLIDirent): 70 """ 71 A CLI package. 72 Must have a `__main__.py` file. 73 """ 74 75 def __init__(self, 76 path: str, 77 qualified_name: str, 78 pymodule: typing.Any, 79 dirents: typing.Union[typing.List[CLIDirent], None] = None, 80 ) -> None: 81 super().__init__(path, qualified_name, pymodule) 82 83 if (dirents is None): 84 dirents = [] 85 86 self.dirents: typing.List[CLIDirent] = dirents 87 """ Entries within this package. """ 88 89 def get_description(self) -> str: 90 if (self.pymodule.__doc__ is None): 91 return '' 92 93 return self.pymodule.__doc__.strip() 94 95 @staticmethod 96 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIPackage', None]: 97 """ Load a representation of the CLI package (or None if the path is not a CLI dirent). """ 98 99 path = os.path.abspath(path) 100 101 if (not os.path.isdir(path)): 102 raise ValueError(f"CLI package path does not point to a dir: '{path}'.") 103 104 main_path = os.path.join(path, '__main__.py') 105 if (not os.path.exists(main_path)): 106 return None 107 108 init_path = os.path.join(path, '__init__.py') 109 if (not os.path.exists(init_path)): 110 return None 111 112 try: 113 init_module = edq.util.pyimport.import_path(init_path) 114 except Exception as ex: 115 raise ValueError(f"Failed to import module __init__.py file: '{init_path}'.") from ex 116 117 package = CLIPackage(path, qualified_name, init_module) 118 119 for dirent in sorted(os.listdir(path)): 120 dirent_path = os.path.join(path, dirent) 121 122 dirent_qualified_name = os.path.splitext(dirent)[0] 123 if (qualified_name != '.'): 124 dirent_qualified_name = qualified_name + '.' + dirent_qualified_name 125 126 cli_dirent = CLIDirent.from_path(dirent_path, dirent_qualified_name) 127 if (cli_dirent is not None): 128 package.dirents.append(cli_dirent) 129 130 return package
A CLI package.
Must have a __main__.py file.
75 def __init__(self, 76 path: str, 77 qualified_name: str, 78 pymodule: typing.Any, 79 dirents: typing.Union[typing.List[CLIDirent], None] = None, 80 ) -> None: 81 super().__init__(path, qualified_name, pymodule) 82 83 if (dirents is None): 84 dirents = [] 85 86 self.dirents: typing.List[CLIDirent] = dirents 87 """ Entries within this package. """
89 def get_description(self) -> str: 90 if (self.pymodule.__doc__ is None): 91 return '' 92 93 return self.pymodule.__doc__.strip()
Get the description of this dirent.
95 @staticmethod 96 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIPackage', None]: 97 """ Load a representation of the CLI package (or None if the path is not a CLI dirent). """ 98 99 path = os.path.abspath(path) 100 101 if (not os.path.isdir(path)): 102 raise ValueError(f"CLI package path does not point to a dir: '{path}'.") 103 104 main_path = os.path.join(path, '__main__.py') 105 if (not os.path.exists(main_path)): 106 return None 107 108 init_path = os.path.join(path, '__init__.py') 109 if (not os.path.exists(init_path)): 110 return None 111 112 try: 113 init_module = edq.util.pyimport.import_path(init_path) 114 except Exception as ex: 115 raise ValueError(f"Failed to import module __init__.py file: '{init_path}'.") from ex 116 117 package = CLIPackage(path, qualified_name, init_module) 118 119 for dirent in sorted(os.listdir(path)): 120 dirent_path = os.path.join(path, dirent) 121 122 dirent_qualified_name = os.path.splitext(dirent)[0] 123 if (qualified_name != '.'): 124 dirent_qualified_name = qualified_name + '.' + dirent_qualified_name 125 126 cli_dirent = CLIDirent.from_path(dirent_path, dirent_qualified_name) 127 if (cli_dirent is not None): 128 package.dirents.append(cli_dirent) 129 130 return package
Load a representation of the CLI package (or None if the path is not a CLI dirent).
Inherited Members
132class CLIModule(CLIDirent): 133 """ 134 A CLI module. 135 Must have the following functions: 136 - `def _get_parser() -> argparse.ArgumentParser:` 137 - `def run_cli(args: argparse.Namespace) -> int:` 138 """ 139 140 def __init__(self, 141 path: str, 142 qualified_name: str, 143 pymodule: typing.Any, 144 parser: argparse.ArgumentParser, 145 ) -> None: 146 super().__init__(path, qualified_name, pymodule) 147 148 self.parser: argparse.ArgumentParser = parser 149 """ The argument parser for this CLI module. """ 150 151 def get_description(self) -> str: 152 if (self.parser.description is None): 153 return '' 154 155 return self.parser.description 156 157 def get_help_text(self) -> str: 158 """ Get the help text from the parser. """ 159 160 buffer = io.StringIO() 161 self.parser.print_help(file = buffer) 162 text = buffer.getvalue() 163 buffer.close() 164 165 return text 166 167 def get_usage_text(self) -> str: 168 """ Get the help text from the parser. """ 169 170 buffer = io.StringIO() 171 self.parser.print_usage(file = buffer) 172 text = buffer.getvalue() 173 buffer.close() 174 175 return text 176 177 @staticmethod 178 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIModule', None]: 179 """ Load a representation of the CLI module (or None if the path is not a CLI dirent). """ 180 181 path = os.path.abspath(path) 182 183 if (not path.endswith('.py')): 184 return None 185 186 if (not os.path.isfile(path)): 187 raise ValueError(f"CLI module path does not point to a file: '{path}'.") 188 189 try: 190 module = edq.util.pyimport.import_path(path) 191 except Exception as ex: 192 raise ValueError(f"Failed to import module: '{path}'.") from ex 193 194 # Check if this looks like a CLI module. 195 if ('_get_parser' not in dir(module)): 196 return None 197 198 parser = module._get_parser() 199 parser.prog = 'python3 -m ' + qualified_name 200 201 return CLIModule(path, qualified_name, module, parser)
A CLI module. Must have the following functions:
def _get_parser() -> argparse.ArgumentParser:def run_cli(args: argparse.Namespace) -> int:
140 def __init__(self, 141 path: str, 142 qualified_name: str, 143 pymodule: typing.Any, 144 parser: argparse.ArgumentParser, 145 ) -> None: 146 super().__init__(path, qualified_name, pymodule) 147 148 self.parser: argparse.ArgumentParser = parser 149 """ The argument parser for this CLI module. """
151 def get_description(self) -> str: 152 if (self.parser.description is None): 153 return '' 154 155 return self.parser.description
Get the description of this dirent.
157 def get_help_text(self) -> str: 158 """ Get the help text from the parser. """ 159 160 buffer = io.StringIO() 161 self.parser.print_help(file = buffer) 162 text = buffer.getvalue() 163 buffer.close() 164 165 return text
Get the help text from the parser.
167 def get_usage_text(self) -> str: 168 """ Get the help text from the parser. """ 169 170 buffer = io.StringIO() 171 self.parser.print_usage(file = buffer) 172 text = buffer.getvalue() 173 buffer.close() 174 175 return text
Get the help text from the parser.
177 @staticmethod 178 def from_path(path: str, qualified_name: str = '.') -> typing.Union['CLIModule', None]: 179 """ Load a representation of the CLI module (or None if the path is not a CLI dirent). """ 180 181 path = os.path.abspath(path) 182 183 if (not path.endswith('.py')): 184 return None 185 186 if (not os.path.isfile(path)): 187 raise ValueError(f"CLI module path does not point to a file: '{path}'.") 188 189 try: 190 module = edq.util.pyimport.import_path(path) 191 except Exception as ex: 192 raise ValueError(f"Failed to import module: '{path}'.") from ex 193 194 # Check if this looks like a CLI module. 195 if ('_get_parser' not in dir(module)): 196 return None 197 198 parser = module._get_parser() 199 parser.prog = 'python3 -m ' + qualified_name 200 201 return CLIModule(path, qualified_name, module, parser)
Load a representation of the CLI module (or None if the path is not a CLI dirent).