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)
class CLIDirent(abc.ABC):
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.

path: str

The path for the given dirent. For a package, this will point to __init__.py.

qualified_name: str

The Python qualified path to this object.

pymodule: Any

The loaded module for the given path.

def base_name(self) -> str:
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.

@abc.abstractmethod
def get_description(self) -> str:
47    @abc.abstractmethod
48    def get_description(self) -> str:
49        """ Get the description of this dirent. """

Get the description of this dirent.

@staticmethod
def from_path( path: str, qualified_name: str = '.') -> Optional[CLIDirent]:
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).

class CLIPackage(CLIDirent):
 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.

CLIPackage( path: str, qualified_name: str, pymodule: Any, dirents: Optional[List[CLIDirent]] = None)
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. """
dirents: List[CLIDirent]

Entries within this package.

def get_description(self) -> str:
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.

@staticmethod
def from_path( path: str, qualified_name: str = '.') -> Optional[CLIPackage]:
 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).

class CLIModule(CLIDirent):
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:
CLIModule( path: str, qualified_name: str, pymodule: Any, parser: argparse.ArgumentParser)
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. """
parser: argparse.ArgumentParser

The argument parser for this CLI module.

def get_description(self) -> str:
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.

def get_help_text(self) -> str:
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.

def get_usage_text(self) -> str:
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.

@staticmethod
def from_path( path: str, qualified_name: str = '.') -> Optional[CLIModule]:
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).