cli.py
1 """Module that contains the command line application.""" 2 3 # Why does this file exist, and why not put this in `__main__`? 4 # 5 # You might be tempted to import things from `__main__` later, 6 # but that will cause problems: the code will get executed twice: 7 # 8 # - When you run `python -m archan` python will execute 9 # `__main__.py` as a script. That means there won't be any 10 # `archan.__main__` in `sys.modules`. 11 # - When you import `__main__` it will get executed again (as a module) because 12 # there's no `archan.__main__` in `sys.modules`. 13 14 from __future__ import annotations 15 16 import argparse 17 import logging 18 import os 19 import sys 20 from typing import Any 21 22 import colorama 23 24 from archan import debug 25 from archan.analysis import Analysis 26 from archan.config import Config 27 from archan.logging import Logger 28 29 logger = Logger.get_logger(__name__) 30 31 32 class _DebugInfo(argparse.Action): 33 def __init__(self, nargs: int | str | None = 0, **kwargs: Any) -> None: 34 super().__init__(nargs=nargs, **kwargs) 35 36 def __call__(self, *args: Any, **kwargs: Any) -> None: # noqa: ARG002 37 debug.print_debug_info() 38 sys.exit(0) 39 40 41 def valid_file(value: str) -> str: 42 """Check if given file exists and is a regular file. 43 44 Parameters: 45 value: Path to the file. 46 47 Raises: 48 ArgumentTypeError: When value not valid. 49 50 Returns: 51 Original value argument. 52 """ 53 if not value: 54 raise argparse.ArgumentTypeError("'' is not a valid file path") 55 if not os.path.exists(value): 56 raise argparse.ArgumentTypeError(f"{value} is not a valid file path") 57 if os.path.isdir(value): 58 raise argparse.ArgumentTypeError(f"{value} is a directory, not a regular file") 59 return value 60 61 62 def valid_level(value: str) -> str: 63 """Validate the logging level argument for the parser. 64 65 Parameters: 66 value: The value provided on the command line. 67 68 Raises: 69 ArgumentTypeError: When value not valid. 70 71 Returns: 72 The validated level. 73 """ 74 value = value.upper() 75 if getattr(logging, value, None) is None: 76 raise argparse.ArgumentTypeError(f"{value} is not a valid level") 77 return value 78 79 80 def get_parser() -> argparse.ArgumentParser: 81 """Return the CLI argument parser. 82 83 Returns: 84 An argparse parser. 85 """ 86 parser = argparse.ArgumentParser( 87 prog="archan", 88 add_help=False, 89 description="Analysis of your architecture strength based on DSM data", 90 ) 91 parser.add_argument( 92 "-c", 93 "--config", 94 action="store", 95 type=valid_file, 96 dest="config_file", 97 metavar="FILE", 98 help="Configuration file to use.", 99 ) 100 parser.add_argument( 101 "-h", 102 "--help", 103 action="help", 104 default=argparse.SUPPRESS, 105 help="Show this help message and exit.", 106 ) 107 parser.add_argument( 108 "-i", 109 "--input", 110 action="store", 111 type=valid_file, 112 dest="input_file", 113 metavar="FILE", 114 help="Input file containing CSV data.", 115 ) 116 parser.add_argument( 117 "-l", 118 "--list-plugins", 119 action="store_true", 120 dest="list_plugins", 121 default=False, 122 help="Show the available plugins. Default: false.", 123 ) 124 parser.add_argument( 125 "--no-color", 126 action="store_true", 127 dest="no_color", 128 default=False, 129 help="Do not use colors. Default: false.", 130 ) 131 parser.add_argument( 132 "--no-config", 133 action="store_true", 134 dest="no_config", 135 default=False, 136 help="Do not load configuration from file. Default: false.", 137 ) 138 parser.add_argument( 139 "-v", 140 "--verbose-level", 141 action="store", 142 dest="level", 143 type=valid_level, 144 default="ERROR", 145 help="Level of verbosity.", 146 ) 147 parser.add_argument( 148 "-V", 149 "--version", 150 action="version", 151 version=f"archan {debug.get_version()}", 152 help="Show the current version of the program and exit.", 153 ) 154 parser.add_argument("--debug-info", action=_DebugInfo, help="Print debug information.") 155 return parser 156 157 158 def main(args: list[str] | None = None) -> int: 159 """Run the main program. 160 161 This function is executed when you type `archan` or `python -m archan`. 162 163 Parameters: 164 args: Arguments passed from the command line. 165 166 Returns: 167 An exit code. 168 """ 169 parser = get_parser() 170 opts = parser.parse_args(args=args) 171 Logger.set_level(opts.level) 172 173 colorama_args = {"autoreset": True} 174 if opts.no_color: 175 colorama_args["strip"] = True 176 colorama.init(**colorama_args) 177 178 config = None 179 if opts.no_config: 180 logger.info("--no-config flag used, use default configuration") 181 if opts.input_file: 182 logger.info(f"Input file specified: {opts.input_file}") 183 file_path = opts.input_file 184 else: 185 logger.info("No input file specified, will read standard input") 186 file_path = None 187 config = Config.default_config(file_path) 188 else: 189 if opts.config_file: 190 logger.info(f"Configuration file specified: {opts.config_file}") 191 config_file = opts.config_file 192 else: 193 logger.info("No configuration file specified, searching") 194 config_file = Config.find() 195 if config_file: 196 logger.info(f"Load configuration from {config_file}") 197 config = Config.from_file(config_file) 198 if config is None: 199 logger.info("No configuration file found, use default one") 200 config = Config.default_config() 201 202 logger.debug(f"Configuration = {config}") 203 logger.debug(f"Plugins loaded = {config.plugins}") 204 205 if opts.list_plugins: 206 logger.info("Print list of plugins") 207 config.print_plugins() 208 return 0 209 210 logger.info("Run analysis") 211 analysis = Analysis(config) 212 try: 213 analysis.run(verbose=False) 214 except KeyboardInterrupt: 215 logger.info("Keyboard interruption, aborting") 216 return 130 217 logger.info(f"Analysis successful: {analysis.successful}") 218 logger.info("Output results as TAP") 219 analysis.output_tap() 220 return 0 if analysis.successful else 1