1#!/usr/bin/env python3 2""" 3# Created: Mon Jun 20 14:35:58 2022 (-0400) 4# @author: Jacob Faibussowitsch 5""" 6from __future__ import annotations 7 8import os 9import sys 10 11if __name__ == '__main__': 12 # insert the parent directory into the sys path, otherwise import petsclinter does not 13 # work! 14 sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) 15 16import petsclinter as pl 17import enum 18import pathlib 19import argparse 20 21from petsclinter._error import ClobberTestOutputError 22from petsclinter._typing import * 23 24@enum.unique 25class ReturnCode(enum.IntFlag): 26 SUCCESS = 0 27 ERROR_WERROR = enum.auto() 28 ERROR_ERROR_FIXED = enum.auto() 29 ERROR_ERROR_LEFT = enum.auto() 30 ERROR_ERROR_TEST = enum.auto() 31 ERROR_TEST_FAILED = enum.auto() 32 33__AT_SRC__ = '__at_src__' 34 35def __sanitize_petsc_dir(petsc_dir: StrPathLike) -> Path: 36 petsc_dir = pl.Path(petsc_dir).resolve(strict=True) 37 if not petsc_dir.is_dir(): 38 raise NotADirectoryError(f'PETSC_DIR: {petsc_dir} is not a directory!') 39 return petsc_dir 40 41def __sanitize_src_path(petsc_dir: Path, src_path: Optional[Union[StrPathLike, Iterable[StrPathLike]]]) -> list[Path]: 42 if src_path is None: 43 src_path = [petsc_dir / 'src'] 44 elif isinstance(src_path, pl.Path): 45 src_path = [src_path] 46 elif isinstance(src_path, (str, pathlib.Path)): 47 src_path = [pl.Path(src_path)] 48 elif isinstance(src_path, (list, tuple)): 49 src_path = list(map(pl.Path, src_path)) 50 else: 51 raise TypeError(f'Source path must be a list or tuple, not {type(src_path)}') 52 53 # type checkers still believe that src_path could be a list of strings after this point 54 # for whatever reason 55 return [p.resolve(strict=True) for p in TYPE_CAST(List[pl.Path], src_path)] 56 57def __sanitize_patch_dir(petsc_dir: Path, patch_dir: Optional[StrPathLike]) -> Path: 58 patch_dir = petsc_dir / 'petscLintPatches' if patch_dir is None else pl.Path(patch_dir).resolve() 59 if patch_dir.exists() and not patch_dir.is_dir(): 60 raise NotADirectoryError( 61 f'Patch Directory (as the name suggests) must be a directory, not {patch_dir}' 62 ) 63 return patch_dir 64 65def __sanitize_test_output_dir(src_path: list[Path], test_output_dir: Optional[StrPathLike]) -> Optional[Path]: 66 if isinstance(test_output_dir, str): 67 if test_output_dir != __AT_SRC__: 68 raise ValueError( 69 f'The only allowed string value for test_output_dir is \'{__AT_SRC__}\', don\'t know what to ' 70 f'do with {test_output_dir}' 71 ) 72 if len(src_path) != 1: 73 raise ValueError( 74 f'Can only use default test output dir for single file or directory, not {len(src_path)}' 75 ) 76 77 test_src_path = src_path[0] 78 if test_src_path.is_dir(): 79 test_output_dir = test_src_path / 'output' 80 elif test_src_path.is_file(): 81 test_output_dir = test_src_path.parent / 'output' 82 else: 83 raise RuntimeError(f'Got neither a directory or file as src_path {test_src_path}') 84 85 if test_output_dir is not None: 86 if not test_output_dir.exists(): 87 raise RuntimeError(f'Test Output Directory {test_output_dir} does not appear to exist') 88 test_output_dir = pl.Path(test_output_dir) 89 90 return test_output_dir 91 92def __sanitize_compiler_flags(petsc_dir: Path, petsc_arch: str, verbose: int, extra_compiler_flags: Optional[list[str]]) -> list[str]: 93 if extra_compiler_flags is None: 94 extra_compiler_flags = [] 95 96 return pl.util.build_compiler_flags( 97 petsc_dir, petsc_arch, extra_compiler_flags=extra_compiler_flags, verbose=verbose 98 ) 99 100def main( 101 petsc_dir: StrPathLike, 102 petsc_arch: str, 103 src_path: Optional[Union[StrPathLike, Iterable[StrPathLike]]] = None, 104 clang_dir: Optional[StrPathLike] = None, 105 clang_lib: Optional[StrPathLike] = None, 106 clang_compat_check: bool = True, 107 verbose: int = 0, 108 workers: int = -1, 109 check_function_filter: Optional[Collection[str]] = None, 110 patch_dir: Optional[StrPathLike] = None, 111 apply_patches: bool = False, 112 extra_compiler_flags: Optional[list[str]] = None, 113 extra_header_includes: Optional[list[str]] = None, 114 test_output_dir: Optional[StrPathLike] = None, 115 replace_tests: bool = False, 116 werror: bool = False 117) -> int: 118 r"""Entry point for linter 119 120 Parameters 121 ---------- 122 petsc_dir : 123 $PETSC_DIR 124 petsc_arch : 125 $PETSC_ARCH 126 src_path : optional 127 directory (or file) to lint (default: $PETSC_DIR/src) 128 clang_dir : optional 129 directory containing libclang.[so|dylib|dll] (default: None) 130 clang_lib : optional 131 direct path to libclang.[so|dylib|dll], overrides clang_dir if set (default: None) 132 clang_compat_check : optional 133 do clang lib compatibility check 134 verbose : optional 135 display debugging statements (default: False) 136 workers : optional 137 number of processes for multiprocessing, -1 is number of system CPU's-1, 0 or 1 for serial 138 computation (default: -1) 139 check_function_filter : optional 140 list of function names as strings to only check for, none == all of them. For example 141 ["PetscAssertPointer", "PetscValidHeaderSpecific"] (default: None) 142 patch_dir : optional 143 directory to store patches if they are generated (default: $PETSC_DIR/petscLintPatches) 144 apply_patches : optional 145 automatically apply patch files to source if they are generated (default: False) 146 extra_compiler_flags : optional 147 list of extra compiler flags to append to PETSc and system flags. 148 For example ["-I/my/non/standard/include","-Wsome_warning"] (default: None) 149 extra_header_includes : optional 150 list of #include statements to append to the precompiled mega-header, these must be in the 151 include search path. Use extra_compiler_flags to make any other search path additions. 152 For example ["#include <slepc/private/epsimpl.h>"] (default: None) 153 test_output_dir : optional 154 directory containing test output to compare patches against, use special keyword '__at_src__' to 155 use src_path/output (default: None) 156 replace_tests : optional 157 replace output files in test_output_dir with patches generated (default: False) 158 werror : optional 159 treat all linter-generated warnings as errors (default: False) 160 161 Returns 162 ------- 163 ret : 164 an integer returncode corresponding to `ReturnCode` to indicate success or error 165 166 Raises 167 ------ 168 ClobberTestOutputError 169 if `apply_patches` and `test_output_dir` are both truthy, as it is not a good idea to clobber the 170 test files 171 TypeError 172 if `src_path` is not a `Path`, str, or list/tuple thereof 173 FileNotFoundError 174 if any of the paths in `src_path` do not exist 175 NotADirectoryError 176 if `patch_dir` or `petsc_dir` are not a directories 177 ValueError 178 - if `test_output_dir` is '__at_src__' and the number of `src_path`s > 1, since that would make 179 '__at_src__' (i.e. find output directly at `src_path / 'output'`) ambigious 180 - if `test_output_dir` is a str, but not '__at_src__' 181 """ 182 if extra_header_includes is None: 183 extra_header_includes = [] 184 185 def root_sync_print(*args, **kwargs) -> None: 186 if args or kwargs: 187 print('[ROOT]', *args, **kwargs) 188 return 189 pl.sync_print = root_sync_print 190 191 # pre-processing setup 192 if bool(apply_patches) and bool(test_output_dir): 193 raise ClobberTestOutputError('Test directory and apply patches are both non-zero. It is probably not a good idea to apply patches over the test directory!') 194 195 pl.util.initialize_libclang(clang_dir=clang_dir, clang_lib=clang_lib, compat_check=clang_compat_check) 196 petsc_dir = __sanitize_petsc_dir(petsc_dir) 197 src_path = __sanitize_src_path(petsc_dir, src_path) 198 patch_dir = __sanitize_patch_dir(petsc_dir, patch_dir) 199 test_output_dir = __sanitize_test_output_dir(src_path, test_output_dir) 200 compiler_flags = __sanitize_compiler_flags(petsc_dir, petsc_arch, verbose, extra_compiler_flags) 201 202 if len(src_path) == 1 and src_path[0].is_file(): 203 if verbose: 204 pl.sync_print(f'Only processing a single file ({src_path[0]}), setting number of workers to 1') 205 workers = 1 206 207 if check_function_filter is not None: 208 pl.checks.filter_check_function_map(check_function_filter) 209 210 with pl.util.PrecompiledHeader.from_flags( 211 petsc_dir, compiler_flags, extra_header_includes=extra_header_includes, verbose=verbose 212 ): 213 warnings, errors_left, errors_fixed, patches = pl.WorkerPool( 214 workers, verbose=verbose 215 ).setup(compiler_flags, clang_compat_check=clang_compat_check, werror=werror).walk( 216 src_path 217 ).finalize() 218 219 if test_output_dir is not None: 220 from petsclinter.test_main import test_main 221 222 assert len(src_path) == 1 223 # reset the printer 224 pl.sync_print = print 225 sys.stdout.flush() 226 return test_main( 227 petsc_dir, src_path[0], test_output_dir, patches, errors_fixed, errors_left, replace=replace_tests 228 ) 229 elif patches: 230 import time 231 import shutil 232 233 patch_dir.mkdir(exist_ok=True) 234 mangle_postfix = f'_{int(time.time())}.patch' 235 root_dir = f'--directory={patch_dir.anchor}' 236 patch_exec = shutil.which('patch') 237 238 if patch_exec is None: 239 # couldn't find it, but let's just try out the bare name and hope it works, 240 # otherwise this will error below anyways 241 patch_exec = 'patch' 242 243 for fname, patch in patches: 244 # mangled_rel = fname.append_name(mangle_postfix) 245 # assert mangled_rel.parent == src_path[0].parent 246 # not in same directory 247 # mangled_rel = mangled_rel.relative_to(src_path) 248 mangled_file = patch_dir / str(fname.append_name(mangle_postfix)).replace(os.path.sep, '_') 249 if verbose: pl.sync_print('Writing patch to file', mangled_file) 250 mangled_file.write_text(patch) 251 252 if apply_patches: 253 if verbose: pl.sync_print('Applying patches from patch directory', patch_dir) 254 for patch_file in patch_dir.glob('*' + mangle_postfix): 255 if verbose: pl.sync_print('Applying patch', patch_file) 256 output = pl.util.subprocess_capture_output( 257 [patch_exec, root_dir, '--strip=0', '--unified', f'--input={patch_file}'] 258 ) 259 if verbose: pl.sync_print(output.stdout) 260 261 def flatten_diags(diag_list: list[CondensedDiags]) -> str: 262 return '\n'.join( 263 mess 264 for diags in diag_list 265 for dlist in diags.values() 266 for mess in dlist 267 ) 268 269 ret = ReturnCode.SUCCESS 270 format_str = '{:=^85}' 271 if warnings: 272 if verbose: 273 pl.sync_print(format_str.format(' Found Warnings ')) 274 pl.sync_print(flatten_diags(warnings)) 275 pl.sync_print(format_str.format(' End warnings ')) 276 if werror: 277 ret |= ReturnCode.ERROR_WERROR 278 if errors_fixed: 279 if verbose: 280 pl.sync_print(format_str.format(' Fixed Errors ' if apply_patches else ' Fixable Errors ')) 281 pl.sync_print(flatten_diags(errors_fixed)) 282 pl.sync_print(format_str.format(' End Fixed Errors ')) 283 ret |= ReturnCode.ERROR_ERROR_FIXED 284 if errors_left: 285 pl.sync_print(format_str.format(' Unfixable Errors ')) 286 pl.sync_print(flatten_diags(errors_left)) 287 pl.sync_print(format_str.format(' End Unfixable Errors ')) 288 pl.sync_print('Some errors or warnings could not be automatically corrected via the patch files') 289 ret |= ReturnCode.ERROR_ERROR_LEFT 290 if patches: 291 if apply_patches: 292 pl.sync_print('All fixable errors or warnings successfully patched') 293 if ret == ReturnCode.ERROR_ERROR_FIXED: 294 # if the only error is fixed errors, then we don't actually have an error 295 ret = ReturnCode.SUCCESS 296 else: 297 pl.sync_print('Patch files written to', patch_dir) 298 pl.sync_print('Apply manually using:') 299 pl.sync_print( 300 f' for patch_file in {patch_dir / ("*" + mangle_postfix)}; do {patch_exec} {root_dir} --strip=0 --unified --input=${{patch_file}}; done' 301 ) 302 assert ret != ReturnCode.SUCCESS 303 return int(ret) 304 305__ADVANCED_HELP_FLAG__ = '--help-hidden' 306 307def __build_arg_parser(parent_parsers: Optional[list[argparse.ArgumentParser]] = None, advanced_help: bool = False) -> tuple[argparse.ArgumentParser, set[str]]: 308 r"""Build an argument parser which will produce the necessary arguments to call `main()` 309 310 Parameters 311 ---------- 312 parent_parsers : optional 313 a list of parent parsers to construct this parser object from 314 advanced_help : optional 315 whether the parser should emit 'advanced' help options 316 317 Returns 318 ------- 319 parser : 320 the constructed parser 321 all_diagnostics : 322 a set containing every registered diagnostic flag 323 """ 324 class ParserLike(Protocol): 325 def add_argument(self, *args, **kwargs) -> argparse.Action: ... 326 327 def add_advanced_argument(prsr: ParserLike, *args, **kwargs) -> argparse.Action: 328 if not advanced_help: 329 kwargs['help'] = argparse.SUPPRESS 330 return prsr.add_argument(*args, **kwargs) 331 332 def add_bool_argument(prsr: ParserLike, *args, advanced: bool = False, **kwargs) -> argparse.Action: 333 def str2bool(v: Union[str, bool]) -> bool: 334 if isinstance(v, bool): 335 return v 336 v = v.casefold() 337 if v in {'yes', 'true', 't', 'y', '1'}: 338 return True 339 if v in {'no', 'false', 'f', 'n', '0', ''}: 340 return False 341 raise argparse.ArgumentTypeError(f'Boolean value expected, got \'{v}\'') 342 343 kwargs.setdefault('nargs', '?') 344 kwargs.setdefault('const', True) 345 kwargs.setdefault('default', False) 346 kwargs.setdefault('metavar', 'bool') 347 kwargs['type'] = str2bool 348 if advanced: 349 return add_advanced_argument(prsr, *args, **kwargs) 350 return prsr.add_argument(*args, **kwargs) 351 352 if parent_parsers is None: 353 parent_parsers = [] 354 355 clang_dir = pl.util.try_to_find_libclang_dir() 356 try: 357 petsc_dir = os.environ['PETSC_DIR'] 358 default_src_dir = str(pl.Path(petsc_dir).resolve() / 'src') 359 except KeyError: 360 petsc_dir = None 361 default_src_dir = '$PETSC_DIR/src' 362 try: 363 petsc_arch = os.environ['PETSC_ARCH'] 364 except KeyError: 365 petsc_arch = None 366 367 parser = argparse.ArgumentParser( 368 prog='petsclinter', 369 description='set options for clang static analysis tool', 370 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 371 parents=parent_parsers 372 ) 373 374 # don't use an argument group for this so it appears directly next to default --help 375 # description! 376 add_bool_argument( 377 parser, __ADVANCED_HELP_FLAG__, help='show more help output (e.g. the various check flags)' 378 ) 379 380 def str2int(v: str) -> int: 381 v = v.strip() 382 if v == '': 383 # for the case of --option=${SOME_MAKE_VAR} where SOME_MAKE_VAR is empty/undefined 384 ret = 0 385 else: 386 ret = int(v) 387 if ret < 0: 388 raise ValueError(f'Integer argument {v} must be >= 0') 389 return ret 390 391 group_general = parser.add_argument_group(title='General options') 392 group_general.add_argument('--version', action='version', version=f'%(prog)s {pl.version_str()}') 393 group_general.add_argument('-v', '--verbose', nargs='?', type=str2int, const=1, default=0, help='verbose progress printed to screen, must be >= 0') 394 add_bool_argument(group_general, '--pm', help='launch an IPython post_mortem() on any raised exceptions (implies -j/--jobs 1)') 395 add_bool_argument(group_general, '--werror', help='treat all warnings as errors') 396 group_general.add_argument('-j', '--jobs', type=int, const=-1, default=-1, nargs='?', help='number of multiprocessing jobs, -1 means number of processors on machine', dest='workers') 397 group_general.add_argument('-p', '--patch-dir', help='directory to store patches in if they are generated, defaults to SRC_DIR/../petscLintPatches', dest='patch_dir') 398 add_bool_argument(group_general, '-a', '--apply-patches', help='automatically apply patches that are saved to file', dest='apply_patches') 399 group_general.add_argument('--CXXFLAGS', nargs='+', default=[], help='extra flags to pass to CXX compiler', dest='extra_compiler_flags') 400 group_general.add_argument('--INCLUDEFLAGS', nargs='+', default=[], help='extra include flags to pass to CXX compiler', dest='extra_header_includes') 401 402 group_libclang = parser.add_argument_group(title='libClang location settings') 403 add_bool_argument(group_libclang, '--clang-compat-check', default=True, help='enable clang compatibility check') 404 group = group_libclang.add_mutually_exclusive_group(required=False) 405 group.add_argument('--clang_dir', metavar='path', type=pl.Path, nargs='?', default=clang_dir, help='directory containing libclang.[so|dylib|dll], if not given attempts to automatically detect it via llvm-config', dest='clang_dir') 406 group.add_argument('--clang_lib', metavar='path', type=pl.Path, nargs='?', help='direct location of libclang.[so|dylib|dll], overrides clang directory if set', dest='clang_lib') 407 408 group_petsc = parser.add_argument_group(title='PETSc location settings') 409 group_petsc.add_argument('--PETSC_DIR', default=petsc_dir, help='if this option is unused defaults to environment variable $PETSC_DIR', dest='petsc_dir') 410 group_petsc.add_argument('--PETSC_ARCH', default=petsc_arch, help='if this option is unused defaults to environment variable $PETSC_ARCH', dest='petsc_arch') 411 412 group_test = parser.add_argument_group(title='Testing settings') 413 group_test.add_argument('--test', metavar='path', nargs='?', const=__AT_SRC__, help='test the linter for correctness. Optionally provide a directory containing the files against which to compare patches, defaults to SRC_DIR/output if no argument is given. The files of correct patches must be in the format [path_from_src_dir_to_testFileName].out', dest='test_output_dir') 414 add_bool_argument(group_test, '--replace', help='replace output files in test directory with patches generated', dest='replace_tests') 415 416 group_diag = parser.add_argument_group(title='Diagnostics settings') 417 check_function_map_keys = list(pl.checks._register.check_function_map.keys()) 418 filter_func_choices = ', '.join(check_function_map_keys) 419 add_advanced_argument(group_diag, '--functions', nargs='+', choices=check_function_map_keys, metavar='FUNCTIONNAME', help='filter to display errors only related to list of provided function names, default is all functions. Choose from available function names: '+filter_func_choices, dest='check_function_filter') 420 421 class CheckFilter(argparse.Action): 422 def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Union[str, bool, Sequence[Any], None], *args, **kwargs) -> None: 423 assert isinstance(values, bool) 424 flag = self.dest.replace(pl.DiagnosticManager.flagprefix[1:], '', 1).replace('_', '-') 425 if flag == 'diagnostics-all': 426 for diag, _ in pl.DiagnosticManager.registered().items(): 427 pl.DiagnosticManager.set(diag, values) 428 else: 429 pl.DiagnosticManager.set(flag, values) 430 setattr(namespace, flag, values) 431 return 432 433 add_bool_argument( 434 group_diag, '-fdiagnostics-all', default=True, action=CheckFilter, advanced=True, 435 help='enable all diagnostics' 436 ) 437 438 all_diagnostics = set() 439 flag_prefix = pl.DiagnosticManager.flagprefix 440 for diag, helpstr in sorted(pl.DiagnosticManager.registered().items()): 441 diag_flag = f'{flag_prefix}{diag}' 442 add_bool_argument( 443 group_diag, diag_flag, default=True, action=CheckFilter, advanced=True, help=helpstr 444 ) 445 all_diagnostics.add(diag_flag) 446 447 parser.add_argument('src_path', default=default_src_dir, help='path to files or directory containing source (e.g. $SLEPC_DIR/src)', nargs='*') 448 return parser, all_diagnostics 449 450def parse_command_line_args(argv: Optional[list[str]] = None, parent_parsers: Optional[list[argparse.ArgumentParser]] = None) -> tuple[argparse.Namespace, argparse.ArgumentParser]: 451 r"""Parse command line argument and return the results 452 453 Parameters 454 ---------- 455 argv : optional 456 the raw command line arguments to parse, defaults to `sys.argv` 457 parent_parsers : optional 458 a set of parent parsers from which to construct the argument parser 459 460 Returns 461 ------- 462 ns : 463 a `argparse.Namespace` object containing the results of the argument parsing 464 parser : 465 the construct `argparse.ArgumentParser` responsible for producing `ns` 466 467 Raises 468 ------ 469 RuntimeError 470 if `args.petsc_dir` or `args.petsc_arch` are None 471 """ 472 def expand_argv_globs(in_argv: list[str], diagnostics: Iterable[str]) -> list[str]: 473 import re 474 475 argv: list[str] = [] 476 skip = False 477 nargv = len(in_argv) 478 flag_prefix = pl.DiagnosticManager.flagprefix 479 # always skip first entry of argv 480 for i, argi in enumerate(in_argv[1:], start=1): 481 if skip: 482 skip = False 483 continue 484 if argi.startswith(flag_prefix) and '*' in argi: 485 if i + 1 >= len(in_argv): 486 parser.error(f'Glob argument {argi} must be followed by explicit value!') 487 488 next_arg = in_argv[i+1] 489 pattern = re.compile(argi.replace('*', '.*')) 490 for flag_to_add in filter(pattern.match, diagnostics): 491 argv.extend((flag_to_add, next_arg)) 492 skip = True 493 else: 494 argv.append(argi) 495 return argv 496 497 if argv is None: 498 argv = sys.argv 499 500 parser, all_diagnostics = __build_arg_parser( 501 parent_parsers=parent_parsers, advanced_help = __ADVANCED_HELP_FLAG__ in argv 502 ) 503 args = parser.parse_args(args=expand_argv_globs(argv, all_diagnostics)) 504 505 if getattr(args, __ADVANCED_HELP_FLAG__.replace('-', '_').lstrip('_')): 506 parser.print_help() 507 parser.exit(0) 508 509 if args.petsc_dir is None: 510 raise RuntimeError('Could not determine PETSC_DIR from environment, please set via options') 511 if args.petsc_arch is None: 512 raise RuntimeError('Could not determine PETSC_ARCH from environment, please set via options') 513 514 if args.clang_lib: 515 args.clang_dir = None 516 517 return args, parser 518 519def namespace_main(args: argparse.Namespace) -> int: 520 r"""The main function for when the linter is invoked from arguments parsed via argparse 521 522 Parameters 523 ---------- 524 args : 525 the result of `argparse.ArgumentParser.parse_args()`, which should have all the options required to 526 call `main()` 527 528 Returns 529 ------- 530 ret : 531 the resultant error code from `main()` 532 """ 533 return main( 534 args.petsc_dir, args.petsc_arch, 535 src_path=args.src_path, 536 clang_dir=args.clang_dir, clang_lib=args.clang_lib, clang_compat_check=args.clang_compat_check, 537 verbose=args.verbose, 538 workers=args.workers, 539 check_function_filter=args.check_function_filter, 540 patch_dir=args.patch_dir, apply_patches=args.apply_patches, 541 extra_compiler_flags=args.extra_compiler_flags, extra_header_includes=args.extra_header_includes, 542 test_output_dir=args.test_output_dir, replace_tests=args.replace_tests, 543 werror=args.werror 544 ) 545 546def command_line_main() -> int: 547 r"""The main function for when the linter is invoked from the command line 548 549 Returns 550 ------- 551 ret : 552 the resultant error code from `main()` 553 """ 554 args, _ = parse_command_line_args() 555 have_pm = args.pm 556 if have_pm: 557 if args.verbose: 558 pl.sync_print('Running with --pm flag, setting number of workers to 1') 559 args.workers = 1 560 try: 561 import ipdb as py_db # type: ignore[import] 562 except ModuleNotFoundError: 563 import pdb as py_db # LINT IGNORE 564 565 try: 566 return namespace_main(args) 567 except: 568 if have_pm: 569 py_db.post_mortem() 570 raise 571 572if __name__ == '__main__': 573 sys.exit(command_line_main()) 574