xref: /petsc/lib/petsc/bin/maint/petsclinter/petsclinter/main.py (revision 09b68a49ed2854d1e4985cc2aa6af33c7c4e69b3)
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