Source code for skysim.__main__

  1"""Calling module for the SkySim package."""
  2
  3# License: GPLv3+ (see COPYING); Copyright (C) 2025 Tai Withers
  4
  5import argparse
  6import sys
  7from pathlib import Path
  8
  9# skysim. is required to run file as python <file>, but not for poetry install
 10# TODO: check requirement for pip install
 11from skysim.plot import create_plot
 12from skysim.populate import create_image_matrix
 13from skysim.query import get_body_locations, get_planet_table, get_star_table
 14from skysim.settings import (
 15    PlotSettings,
 16    load_from_toml,
 17)
 18from skysim.utils import get_tempfile_path, read_pyproject
 19
 20# Methods
 21
 22
 23## Top-Level Methods
 24
 25
[docs] 26def main( # pylint: disable=inconsistent-return-statements 27 args: list[str] | None = None, 28) -> Path | None: 29 """Entrypoint for the SkySim package. 30 31 Calls the following functions directly: 32 33 - :doc:`main` 34 - `parse_args` 35 - `confirm_config_file` 36 - `handle_overwrite` 37 - :doc:`settings` 38 - `~skysim.settings.load_from_toml` 39 - :doc:`query` 40 - `~skysim.query.get_star_table` 41 - `~skysim.query.get_body_locations` 42 - `~skysim.query.get_planet_table` 43 - :doc:`populate` 44 - `~skysim.populate.create_image_matrix` 45 - :doc:`plot` 46 - `~skysim.plot.create_plot` 47 48 Parameters 49 ---------- 50 args : list[str] | None , default None 51 When called via command line, this is None, and argparse captures the arguments 52 directly. 53 When running `main()` inside of python (e.g., with pytest), `args` holds a list 54 of arguments. 55 56 Returns 57 ------- 58 pathlib.Path 59 Path object to the created file. Returned if `--debug` flag is set. 60 None 61 Returned if `--debug` flag is unset. 62 63 Raises 64 ------ 65 ValueError, ConnectionError 66 Re-raised from their creation inside the SkySim code if and only if 67 the --debug flag is set. 68 69 SystemExit 70 Supercedes internal errors to produce a cleaner error message if the 71 --debug flag is not set. 72 """ 73 74 options = parse_cli_args(args) 75 try: 76 config_path = confirm_config_file(options.config_file) 77 78 image_settings, plot_settings = load_from_toml(config_path) 79 80 handle_overwrite(plot_settings, options) 81 82 star_table = get_star_table( 83 image_settings.observation_radec, 84 image_settings.field_of_view, 85 image_settings.maximum_magnitude, 86 image_settings.object_colours, 87 ) 88 89 body_locations = get_body_locations( 90 image_settings.observation_times, image_settings.earth_location 91 ) 92 planet_tables = get_planet_table(body_locations) 93 94 image = create_image_matrix( 95 image_settings, # type: ignore[arg-type] 96 planet_tables, 97 star_table, 98 options.verbose, 99 ) 100 101 create_plot(plot_settings, image, options.verbose) # type: ignore[arg-type] 102 103 # Optionally print simple error message instead of full traceback 104 except (ValueError, ConnectionError) as e: 105 if options.debug: 106 raise e 107 108 sys.stderr.write(f"error: {' '.join(e.args)}") 109 sys.exit(1) 110 111 if options.debug: 112 # note: returning a value means that an exit code of 1 is always returned 113 # therefore --debug should not be used in pipelines 114 return plot_settings.filename 115 116 return # type: ignore[return-value]
117 118 119## Helper Methods 120 121
[docs] 122class CustomArgumentParser(argparse.ArgumentParser): 123 """Override of the default ArgumentParser to change the error message.""" 124
[docs] 125 def error(self, message): 126 self.print_usage() 127 self.exit(2, f"error: {message}")
128 129
[docs] 130def parse_cli_args(args: list[str] | None) -> argparse.Namespace: 131 """Parse command line arguments. 132 133 Parameters 134 ---------- 135 args : list[str] | None 136 Arguments. 137 138 Returns 139 ------- 140 argparse.Namespace 141 Parsed arguments. 142 """ 143 pyproject = read_pyproject() 144 executable = list(pyproject["scripts"].keys())[0] 145 version_string = f"{pyproject['name']} {pyproject['version']}" 146 verbosity_options = {0: "silent", 1: "default", 2: "more output"} 147 default_verbosity = [k for k, v in verbosity_options.items() if v == "default"][0] 148 149 # instantiate the parser with project info 150 parser = CustomArgumentParser( 151 prog=executable, 152 description=pyproject["description"], 153 epilog="\n".join( 154 [ 155 version_string, 156 "Repository: " + pyproject["urls"]["repository"], 157 "License: " + pyproject["license"], 158 ] 159 ), 160 formatter_class=argparse.RawTextHelpFormatter, 161 ) 162 163 # add cli arguments/options 164 parser.add_argument("config_file", help="TOML configuration file") 165 parser.add_argument( 166 "--debug", 167 help="print full Python traceback, causes an exit code of 1, even on success", 168 action="store_true", 169 ) 170 parser.add_argument( 171 "--overwrite", help="overwrite existing file(s)", action="store_true" 172 ) 173 parser.add_argument( 174 "--verbose", 175 help=str(verbosity_options)[1:-1].replace("'", ""), # remove {}, '' 176 choices=verbosity_options.keys(), 177 type=int, # casts input to int before applying choices 178 default=default_verbosity, 179 ) 180 parser.add_argument( 181 "--version", 182 version=version_string, 183 action="version", 184 ) 185 186 return parser.parse_args(args)
187 188
[docs] 189def confirm_config_file(input_config_path: str) -> Path: 190 """Pre-validate the existence of a config file. 191 192 Parameters 193 ---------- 194 input_config_path : str 195 Argument passed on command line. 196 197 Returns 198 ------- 199 pathlib.Path 200 Given config file path (if any). 201 202 Raises 203 ------ 204 ValueError 205 Raised if 206 - Path given does not exist. 207 - Path leads to a non-file object. 208 - Path does not have a ".toml" extension. 209 """ 210 211 config_path = Path(input_config_path).resolve() 212 213 if not config_path.exists(): 214 raise ValueError(f"{config_path} does not exist.") 215 if not config_path.is_file(): 216 raise ValueError(f"{config_path} is not a file.") 217 if config_path.suffix != ".toml": 218 raise ValueError(f"{config_path} does not have a '.toml' extension.") 219 220 return config_path
221 222
[docs] 223def handle_overwrite(plot_settings: PlotSettings, options: argparse.Namespace) -> None: 224 """Check if files will be overwritten and take action based on cli flags (--verbose 225 and --overwrite). 226 227 Parameters 228 ---------- 229 plot_settings : PlotSettings 230 Plotting configuration, passed to `check_for_overwrite`. 231 options : argparse.Namespace 232 Parsed CLI options. 233 234 Raises 235 ------ 236 ValueError 237 Raised if an overwrite would happen but the --overwrite flag was not passed. 238 """ 239 overwritten_paths = check_for_overwrite(plot_settings) 240 241 # noop if no overwrites 242 if len(overwritten_paths) == 0: 243 return 244 245 # if there's mutiple files, start the printout on a new line 246 if len(overwritten_paths) > 1: 247 overwritten_paths.insert(0, "") 248 249 # set the printout based on verbosity 250 if options.verbose > 1: 251 paths_string = "\n".join([str(p) for p in overwritten_paths]) 252 paths_message = f"the following files: {paths_string}." 253 else: 254 paths_message = "one or more files." 255 256 # display message or error based on use of --overwrite 257 if options.overwrite: 258 print(f"Overwriting {paths_message}") 259 else: 260 raise ValueError( 261 f"Running SkySim would overwrite {paths_message}\nUse the --overwrite flag" 262 " or change/remove the output path to continue." 263 )
264 265 266### Secondary Helpers 267 268
[docs] 269def check_for_overwrite(plot_settings: PlotSettings) -> list[Path]: 270 """Check if SkySim will overwrite any existing files. 271 272 Parameters 273 ---------- 274 plot_settings : PlotSettings 275 Configuration. 276 277 Returns 278 ------- 279 pathlib.Path 280 Returns a list of paths that will be overwritten. 281 """ 282 overwrites = [] 283 284 if plot_settings.filename.exists(): 285 overwrites.append(plot_settings.filename) 286 287 if plot_settings.frames > 1: 288 for i in range(plot_settings.frames): 289 tempfile_path = get_tempfile_path(plot_settings, i) 290 if tempfile_path.exists(): 291 overwrites.append(tempfile_path) 292 293 return overwrites