edq.util.cli

Show the CLI tools available in this package.

This will look for objects that "look like" CLI tools. A package looks like a CLI package if it has a __main__.py file. 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:
  1"""
  2Show the CLI tools available in this package.
  3
  4This will look for objects that "look like" CLI tools.
  5A package looks like a CLI package if it has a __main__.py file.
  6A module looks like a CLI tool if it has the following functions:
  7 - `def _get_parser() -> argparse.ArgumentParser:`
  8 - `def run_cli(args: argparse.Namespace) -> int:`
  9"""
 10
 11import argparse
 12import inspect
 13import os
 14
 15import edq.util.dirent
 16import edq.util.pyimport
 17
 18def auto_list(
 19        recursive: bool = False,
 20        skip_dirs: bool = False,
 21        ) -> None:
 22    """
 23    Print the caller's docstring and call _list_dir() on it,
 24    but will figure out the package's docstring, base_dir, and command_prefix automatically.
 25    This will use the inspect library, so only use in places that use code normally.
 26    The first stack frame not in this file will be used.
 27    """
 28
 29    this_path = os.path.realpath(__file__)
 30
 31    caller_frame_info = None
 32    for frame_info in inspect.stack():
 33        if (edq.util.dirent.same(this_path, frame_info.filename)):
 34            # Ignore this file.
 35            continue
 36
 37        caller_frame_info = frame_info
 38        break
 39
 40    if (caller_frame_info is None):
 41        raise ValueError("Unable to determine caller's stack frame.")
 42
 43    path = caller_frame_info.filename
 44    base_dir = os.path.dirname(path)
 45
 46    try:
 47        module = inspect.getmodule(caller_frame_info.frame)
 48        if (module is None):
 49            raise ValueError(f"Unable to get module for '{path}'.")
 50    except Exception as ex:
 51        raise ValueError("Unable to get caller information for listing CLI information.") from ex
 52
 53    if (module.__package__ is None):
 54        raise ValueError(f"Caller module has no package information: '{path}'.")
 55
 56    if (module.__doc__ is None):
 57        raise ValueError(f"Caller module has no docstring: '{path}'.")
 58
 59    print(module.__doc__.strip())
 60    _list_dir(base_dir, module.__package__, recursive, skip_dirs)
 61
 62def _list_dir(base_dir: str, command_prefix: str, recursive: bool, skip_dirs: bool) -> None:
 63    """ List/descend the given dir. """
 64
 65    for dirent in sorted(os.listdir(base_dir)):
 66        path = os.path.join(base_dir, dirent)
 67        cmd = command_prefix + '.' + os.path.splitext(dirent)[0]
 68
 69        if (dirent.startswith('__')):
 70            continue
 71
 72        if (os.path.isfile(path)):
 73            _handle_file(path, cmd)
 74        else:
 75            if (not skip_dirs):
 76                _handle_dir(path, cmd)
 77
 78            if (recursive):
 79                _list_dir(path, cmd, recursive, skip_dirs)
 80
 81def _handle_file(path: str, cmd: str) -> None:
 82    """ Process a file (possible module). """
 83
 84    if (not path.endswith('.py')):
 85        return
 86
 87    try:
 88        module = edq.util.pyimport.import_path(path)
 89    except Exception:
 90        print("ERROR Importing: ", path)
 91        return
 92
 93    if ('_get_parser' not in dir(module)):
 94        return
 95
 96    parser = module._get_parser()
 97    parser.prog = 'python3 -m ' + cmd
 98
 99    print()
100    print(cmd)
101    print(parser.description)
102    parser.print_usage()
103
104def _handle_dir(path: str, cmd: str) -> None:
105    """ Process a dir (possible package). """
106
107    try:
108        module = edq.util.pyimport.import_path(os.path.join(path, '__main__.py'))
109    except Exception:
110        return
111
112    description = module.__doc__.strip()
113
114    print()
115    print(cmd + '.*')
116    print(description)
117    print(f"See `python3 -m {cmd}` for more information.")
118
119def _get_parser() -> argparse.ArgumentParser:
120    parser = argparse.ArgumentParser(
121        description = __doc__.strip(),
122        epilog = ("Note that you don't need to provide a package as an argument,"
123            + " since you already called this on the target package."))
124
125    parser.add_argument('-r', '--recursive', dest = 'recursive',
126        action = 'store_true', default = False,
127        help = 'Recur into each package to look for tools and subpackages (default: %(default)s).')
128
129    parser.add_argument('-s', '--skip-dirs', dest = 'skip_dirs',
130        action = 'store_true', default = False,
131        help = ('Do not output information about directories/packages,'
132            + ' only tools/files/modules (default: %(default)s).'))
133
134    return parser
135
136def run_cli(args: argparse.Namespace) -> int:
137    """
138    List the caller's dir.
139    """
140
141    auto_list(recursive = args.recursive, skip_dirs = args.skip_dirs)
142
143    return 0
144
145def main() -> int:
146    """
147    Run as if this process has been called as a executable.
148    This will parse the command line and list the caller's dir.
149    """
150
151    return run_cli(_get_parser().parse_args())
def auto_list(recursive: bool = False, skip_dirs: bool = False) -> None:
19def auto_list(
20        recursive: bool = False,
21        skip_dirs: bool = False,
22        ) -> None:
23    """
24    Print the caller's docstring and call _list_dir() on it,
25    but will figure out the package's docstring, base_dir, and command_prefix automatically.
26    This will use the inspect library, so only use in places that use code normally.
27    The first stack frame not in this file will be used.
28    """
29
30    this_path = os.path.realpath(__file__)
31
32    caller_frame_info = None
33    for frame_info in inspect.stack():
34        if (edq.util.dirent.same(this_path, frame_info.filename)):
35            # Ignore this file.
36            continue
37
38        caller_frame_info = frame_info
39        break
40
41    if (caller_frame_info is None):
42        raise ValueError("Unable to determine caller's stack frame.")
43
44    path = caller_frame_info.filename
45    base_dir = os.path.dirname(path)
46
47    try:
48        module = inspect.getmodule(caller_frame_info.frame)
49        if (module is None):
50            raise ValueError(f"Unable to get module for '{path}'.")
51    except Exception as ex:
52        raise ValueError("Unable to get caller information for listing CLI information.") from ex
53
54    if (module.__package__ is None):
55        raise ValueError(f"Caller module has no package information: '{path}'.")
56
57    if (module.__doc__ is None):
58        raise ValueError(f"Caller module has no docstring: '{path}'.")
59
60    print(module.__doc__.strip())
61    _list_dir(base_dir, module.__package__, recursive, skip_dirs)

Print the caller's docstring and call _list_dir() on it, but will figure out the package's docstring, base_dir, and command_prefix automatically. This will use the inspect library, so only use in places that use code normally. The first stack frame not in this file will be used.

def run_cli(args: argparse.Namespace) -> int:
137def run_cli(args: argparse.Namespace) -> int:
138    """
139    List the caller's dir.
140    """
141
142    auto_list(recursive = args.recursive, skip_dirs = args.skip_dirs)
143
144    return 0

List the caller's dir.

def main() -> int:
146def main() -> int:
147    """
148    Run as if this process has been called as a executable.
149    This will parse the command line and list the caller's dir.
150    """
151
152    return run_cli(_get_parser().parse_args())

Run as if this process has been called as a executable. This will parse the command line and list the caller's dir.